From b793b455fafe3421aa8ac0de549078a7b70f58bd Mon Sep 17 00:00:00 2001 From: Hugo Trentesaux <hugo.trentesaux@lilo.org> Date: Wed, 15 Nov 2023 11:59:54 +0100 Subject: [PATCH] implement quotas and refund transaction fees (nodes/rust/duniter-v2s!183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * implement quotas implement weights "à l'arrache" benchmarks duniter-account "à l'arrache" implement benchmark logic (not proper benchmarks) fix live tests 🤦â€â™‚ and clippy 🤦â€â™‚🤦â€â™‚ replace quotas by quota everywhere comment unused sections of template remove quota treasury dependency give treasury address as argument typo review tuxmain doc readme rename error DistanceKO to DistanceNotOK merge new owner key and revocation signature merge signature error types rename NewOwnerKeyPayload fix comment make eligibility more explicit implement quotas implement weights "à l'arrache" benchmarks duniter-account "à l'arrache" implement benchmark logic (not proper benchmarks) fix live tests 🤦â€â™‚ and clippy 🤦â€â™‚🤦â€â™‚ replace quotas by quota everywhere comment unused sections of template remove quota treasury dependency give treasury address as argument typo review tuxmain doc readme rename error DistanceKO to DistanceNotOK merge new owner key and revocation signature merge signature error types rename NewOwnerKeyPayload fix comment make eligibility more explicit update metadata fix fix fee multiplier update prevent network discovery + connecting other nodes --- Cargo.lock | 25 ++ Cargo.toml | 1 + docs/api/runtime-calls.md | 417 +++++++++++++----- .../identity_creation.feature | 6 +- .../cucumber-features/oneshot_account.feature | 12 +- .../cucumber-features/transfer_all.feature | 5 +- end2end-tests/tests/common/mod.rs | 2 + live-tests/tests/sanity_gdev.rs | 2 +- node/src/chain_spec/gdev.rs | 9 +- node/src/chain_spec/gen_genesis_data.rs | 16 +- pallets/duniter-account/Cargo.toml | 7 + pallets/duniter-account/README.md | 8 +- pallets/duniter-account/src/benchmarking.rs | 6 +- pallets/duniter-account/src/lib.rs | 177 +++++++- pallets/duniter-account/src/types.rs | 33 +- pallets/duniter-account/src/weights.rs | 12 + pallets/duniter-wot/src/lib.rs | 24 +- pallets/duniter-wot/src/mock.rs | 7 +- pallets/duniter-wot/src/tests.rs | 8 +- pallets/identity/src/benchmarking.rs | 21 +- pallets/identity/src/lib.rs | 81 +++- pallets/identity/src/mock.rs | 7 +- pallets/identity/src/tests.rs | 85 +++- pallets/identity/src/traits.rs | 9 + pallets/identity/src/types.rs | 11 +- pallets/identity/src/weights.rs | 17 + pallets/membership/src/lib.rs | 8 +- pallets/quota/Cargo.toml | 77 ++++ pallets/quota/README.md | 31 ++ pallets/quota/src/benchmarking.rs | 64 +++ pallets/quota/src/lib.rs | 361 +++++++++++++++ pallets/quota/src/mock.rs | 189 ++++++++ pallets/quota/src/tests.rs | 241 ++++++++++ pallets/quota/src/traits.rs | 27 ++ pallets/quota/src/weights.rs | 25 ++ resources/metadata.scale | Bin 131573 -> 133447 bytes runtime/common/Cargo.toml | 2 + runtime/common/src/entities.rs | 30 +- runtime/common/src/pallets_config.rs | 98 ++-- runtime/common/src/weights.rs | 1 + .../src/weights/pallet_duniter_account.rs | 11 + runtime/common/src/weights/pallet_identity.rs | 16 + runtime/common/src/weights/pallet_quota.rs | 95 ++++ runtime/g1/Cargo.toml | 2 + runtime/g1/src/lib.rs | 5 +- runtime/gdev/Cargo.toml | 3 + runtime/gdev/src/lib.rs | 8 +- runtime/gdev/tests/common/mod.rs | 14 +- runtime/gdev/tests/integration_tests.rs | 140 +++++- runtime/gdev/tests/xt_tests.rs | 192 ++++++++ runtime/gtest/Cargo.toml | 6 +- runtime/gtest/src/lib.rs | 5 +- xtask/README.md | 19 +- xtask/res/templates/runtime-calls-category.md | 2 +- 54 files changed, 2394 insertions(+), 286 deletions(-) create mode 100644 pallets/quota/Cargo.toml create mode 100644 pallets/quota/README.md create mode 100644 pallets/quota/src/benchmarking.rs create mode 100644 pallets/quota/src/lib.rs create mode 100644 pallets/quota/src/mock.rs create mode 100644 pallets/quota/src/tests.rs create mode 100644 pallets/quota/src/traits.rs create mode 100644 pallets/quota/src/weights.rs create mode 100644 runtime/common/src/weights/pallet_quota.rs create mode 100644 runtime/gdev/tests/xt_tests.rs diff --git a/Cargo.lock b/Cargo.lock index e16232044..16591d4c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1048,6 +1048,7 @@ dependencies = [ "pallet-preimage", "pallet-provide-randomness", "pallet-proxy", + "pallet-quota", "pallet-scheduler", "pallet-session", "pallet-session-benchmarking", @@ -3004,6 +3005,7 @@ dependencies = [ "pallet-preimage", "pallet-provide-randomness", "pallet-proxy", + "pallet-quota", "pallet-scheduler", "pallet-session", "pallet-sudo", @@ -3072,6 +3074,7 @@ dependencies = [ "pallet-preimage", "pallet-provide-randomness", "pallet-proxy", + "pallet-quota", "pallet-scheduler", "pallet-session", "pallet-session-benchmarking", @@ -3375,6 +3378,7 @@ dependencies = [ "pallet-preimage", "pallet-provide-randomness", "pallet-proxy", + "pallet-quota", "pallet-scheduler", "pallet-session", "pallet-session-benchmarking", @@ -3396,6 +3400,7 @@ dependencies = [ "sp-block-builder", "sp-consensus-babe", "sp-core", + "sp-distance", "sp-inherents", "sp-io", "sp-keyring", @@ -5791,7 +5796,10 @@ dependencies = [ "log", "maplit", "pallet-balances", + "pallet-identity", "pallet-provide-randomness", + "pallet-quota", + "pallet-transaction-payment", "pallet-treasury", "parity-scale-codec", "scale-info", @@ -6028,6 +6036,23 @@ dependencies = [ "sp-std 5.0.0", ] +[[package]] +name = "pallet-quota" +version = "3.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-identity", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std 5.0.0", +] + [[package]] name = "pallet-scheduler" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index a8064f606..139256e47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -145,6 +145,7 @@ members = [ 'end2end-tests', 'live-tests', 'pallets/certification', + 'pallets/quota', 'pallets/distance', 'pallets/duniter-test-parameters', 'pallets/duniter-test-parameters/macro', diff --git a/docs/api/runtime-calls.md b/docs/api/runtime-calls.md index 95ffd5343..f6a2ddb8a 100644 --- a/docs/api/runtime-calls.md +++ b/docs/api/runtime-calls.md @@ -13,7 +13,20 @@ through on-chain governance mechanisms. ## User calls -There are **69** user calls from **21** pallets. +There are **80** user calls from **23** pallets. + +### Account - 1 + +#### unlink_identity - 0 + +<details><summary><code>unlink_identity()</code></summary> + +```rust +``` +</details> + + +unlink the identity associated with the account ### Scheduler - 2 @@ -88,7 +101,6 @@ call: Box<<T as Config>::RuntimeCall> Anonymously schedule a task after a delay. - #### schedule_named_after - 5 <details><summary><code>schedule_named_after(id, after, maybe_periodic, priority, call)</code></summary> @@ -105,7 +117,6 @@ call: Box<<T as Config>::RuntimeCall> Schedule a named task after a delay. - ### Babe - 3 #### report_equivocation - 0 @@ -126,9 +137,9 @@ be reported. ### Balances - 6 -#### transfer - 0 +#### transfer_allow_death - 0 -<details><summary><code>transfer(dest, value)</code></summary> +<details><summary><code>transfer_allow_death(dest, value)</code></summary> ```rust dest: AccountIdLookupOf<T> @@ -139,12 +150,30 @@ value: T::Balance Transfer some liquid free balance to another account. -`transfer` will set the `FreeBalance` of the sender and receiver. +`transfer_allow_death` will set the `FreeBalance` of the sender and receiver. If the sender's account is below the existential deposit as a result of the transfer, the account will be reaped. The dispatch origin for this call must be `Signed` by the transactor. +#### set_balance_deprecated - 1 + +<details><summary><code>set_balance_deprecated(who, new_free, old_reserved)</code></summary> + +```rust +who: AccountIdLookupOf<T> +new_free: T::Balance +old_reserved: T::Balance +``` +</details> + + +Set the regular balance of a given account; it also takes a reserved balance but this +must be the same as the account's current reserved balance. + +The dispatch origin for this call is `root`. + +WARNING: This call is DEPRECATED! Use `force_set_balance` instead. #### transfer_keep_alive - 3 @@ -157,12 +186,12 @@ value: T::Balance </details> -Same as the [`transfer`] call, but with a check that the transfer will not kill the -origin account. +Same as the [`transfer_allow_death`] call, but with a check that the transfer will not +kill the origin account. -99% of the time you want [`transfer`] instead. +99% of the time you want [`transfer_allow_death`] instead. -[`transfer`]: struct.Pallet.html#method.transfer +[`transfer_allow_death`]: struct.Pallet.html#method.transfer #### transfer_all - 4 @@ -189,9 +218,56 @@ The dispatch origin of this call must be Signed. - `keep_alive`: A boolean to determine if the `transfer_all` operation should send all of the funds the account has, causing the sender account to be killed (false), or transfer everything except at least the existential deposit, which will guarantee to - keep the sender account alive (true). # <weight> -- O(1). Just like transfer, but reading the user's transferable balance first. - #</weight> + keep the sender account alive (true). + +#### upgrade_accounts - 6 + +<details><summary><code>upgrade_accounts(who)</code></summary> + +```rust +who: Vec<T::AccountId> +``` +</details> + + +Upgrade a specified account. + +- `origin`: Must be `Signed`. +- `who`: The account to be upgraded. + +This will waive the transaction fee if at least all but 10% of the accounts needed to +be upgraded. (We let some not have to be upgraded just in order to allow for the +possibililty of churn). + +#### transfer - 7 + +<details><summary><code>transfer(dest, value)</code></summary> + +```rust +dest: AccountIdLookupOf<T> +value: T::Balance +``` +</details> + + +Alias for `transfer_allow_death`, provided only for name-wise compatibility. + +WARNING: DEPRECATED! Will be released in approximately 3 months. + +#### force_set_balance - 8 + +<details><summary><code>force_set_balance(who, new_free)</code></summary> + +```rust +who: AccountIdLookupOf<T> +new_free: T::Balance +``` +</details> + + +Set the regular balance of a given account. + +The dispatch origin for this call is `root`. ### OneshotAccount - 7 @@ -290,6 +366,18 @@ keys: T::KeysWrapper declare new session keys to replace current ones +#### remove_member_from_blacklist - 4 + +<details><summary><code>remove_member_from_blacklist(member_id)</code></summary> + +```rust +member_id: T::MemberId +``` +</details> + + +remove an identity from the blacklist + ### Grandpa - 15 #### report_equivocation - 0 @@ -407,6 +495,11 @@ Dispatch a proposal from a member using the `Member` origin. Origin must be a member of the collective. +**Complexity**: +- `O(B + M + P)` where: +- `B` is `proposal` size in bytes (length-fee-bounded) +- `M` members-count (code-bounded) +- `P` complexity of dispatching `proposal` #### propose - 2 @@ -427,6 +520,13 @@ Requires the sender to be member. `threshold` determines whether `proposal` is executed directly (`threshold < 2`) or put up for voting. +**Complexity** +- `O(B + M + P1)` or `O(B + M + P2)` where: + - `B` is `proposal` size in bytes (length-fee-bounded) + - `M` is members-count (code- and governance-bounded) + - branching is influenced by `threshold` where: + - `P1` is proposal execution complexity (`threshold < 2`) + - `P2` is proposals-count (code-bounded) (`threshold >= 2`) #### vote - 3 @@ -447,38 +547,8 @@ Requires the sender to be a member. Transaction fees will be waived if the member is voting on any particular proposal for the first time and the call is successful. Subsequent vote changes will charge a fee. - -#### close_old_weight - 4 - -<details><summary><code>close_old_weight(proposal_hash, index, proposal_weight_bound, length_bound)</code></summary> - -```rust -proposal_hash: T::Hash -index: ProposalIndex -proposal_weight_bound: OldWeight -length_bound: u32 -``` -</details> - - -Close a vote that is either approved, disapproved or whose voting period has ended. - -May be called by any signed account in order to finish voting and close the proposal. - -If called before the end of the voting period it will only close the vote if it is -has enough votes to be approved or disapproved. - -If called after the end of the voting period abstentions are counted as rejections -unless there is a prime member set and the prime member cast an approval. - -If the close operation completes successfully with disapproval, the transaction fee will -be waived. Otherwise execution of the approved operation will be charged to the caller. - -+ `proposal_weight_bound`: The maximum amount of weight consumed by executing the closed -proposal. -+ `length_bound`: The upper bound for the length of the proposal in storage. Checked via -`storage::read` so it is `size_of::<u32>() == 4` larger than the pure length. - +**Complexity** +- `O(M)` where `M` is members-count (code- and governance-bounded) #### close - 6 @@ -511,6 +581,12 @@ proposal. + `length_bound`: The upper bound for the length of the proposal in storage. Checked via `storage::read` so it is `size_of::<u32>() == 4` larger than the pure length. +**Complexity** +- `O(B + M + P1 + P2)` where: + - `B` is `proposal` size in bytes (length-fee-bounded) + - `M` is members-count (code- and governance-bounded) + - `P1` is the complexity of `proposal` preimage. + - `P2` is proposal-count (code-bounded) ### UniversalDividend - 30 @@ -603,7 +679,7 @@ validate the owned identity (must meet the main wot requirements) ```rust new_key: T::AccountId -new_key_sig: T::NewOwnerKeySignature +new_key_sig: T::Signature ``` </details> @@ -611,7 +687,7 @@ new_key_sig: T::NewOwnerKeySignature Change identity owner key. - `new_key`: the new owner key. -- `new_key_sig`: the signature of the encoded form of `NewOwnerKeyPayload`. +- `new_key_sig`: the signature of the encoded form of `IdtyIndexAccountIdPayload`. Must be signed by `new_key`. The origin should be the old identity owner key. @@ -623,7 +699,7 @@ The origin should be the old identity owner key. ```rust idty_index: T::IdtyIndex revocation_key: T::AccountId -revocation_sig: T::RevocationSignature +revocation_sig: T::Signature ``` </details> @@ -650,6 +726,19 @@ inc: bool change sufficient ref count for given key +#### link_account - 8 + +<details><summary><code>link_account(account_id, payload_sig)</code></summary> + +```rust +account_id: T::AccountId +payload_sig: T::Signature +``` +</details> + + +Link an account to an identity + ### Membership - 42 #### claim_membership - 1 @@ -694,6 +783,63 @@ Add a new certification or renew an existing one The origin must be allow to certify. +### Distance - 44 + +#### request_distance_evaluation - 0 + +<details><summary><code>request_distance_evaluation()</code></summary> + +```rust +``` +</details> + + +Request an identity to be evaluated + +#### update_evaluation - 1 + +<details><summary><code>update_evaluation(computation_result)</code></summary> + +```rust +computation_result: ComputationResult +``` +</details> + + +(Inherent) Push an evaluation result to the pool + +#### force_update_evaluation - 2 + +<details><summary><code>force_update_evaluation(evaluator, computation_result)</code></summary> + +```rust +evaluator: <T as frame_system::Config>::AccountId +computation_result: ComputationResult +``` +</details> + + +Push an evaluation result to the pool + +#### force_set_distance_status - 3 + +<details><summary><code>force_set_distance_status(identity, status)</code></summary> + +```rust +identity: <T as pallet_identity::Config>::IdtyIndex +status: Option<(<T as frame_system::Config>::AccountId, DistanceStatus)> +``` +</details> + + +Set the distance evaluation status of an identity + +Removes the status if `status` is `None`. + +* `status.0` is the account for whom the price will be unreserved or slashed + when the evaluation completes. +* `status.1` is the status of the evaluation. + ### SmithMembership - 52 #### request_membership - 0 @@ -851,6 +997,8 @@ multi-signature, but do not participate in the approval process. Result is equivalent to the dispatched result. +**Complexity** +O(Z + C) where Z is the length of the call and C its execution weight. #### as_multi - 1 @@ -892,6 +1040,19 @@ Result is equivalent to the dispatched result if `threshold` is exactly `1`. Oth on success, result is `Ok` and the result from the interior call, if it was executed, may be found in the deposited `MultisigExecuted` event. +**Complexity** +- `O(S + Z + Call)`. +- Up to one balance-reserve or unreserve operation. +- One passthrough operation, one insert, both `O(S)` where `S` is the number of + signatories. `S` is capped by `MaxSignatories`, with weight being proportional. +- One call encode & hash, both of complexity `O(Z)` where `Z` is tx-len. +- One encode & hash, both of complexity `O(S)`. +- Up to one binary search and insert (`O(logS + S)`). +- I/O: 1 read `O(S)`, up to 1 mutate `O(S)`. Up to one remove. +- One event. +- The weight of the `call`. +- Storage: inserts one item, value size bounded by `MaxSignatories`, with a deposit + taken for its lifetime of `DepositBase + threshold * DepositFactor`. #### approve_as_multi - 2 @@ -926,6 +1087,17 @@ transaction index) of the first approval transaction. NOTE: If this is the final approval, you will want to use `as_multi` instead. +**Complexity** +- `O(S)`. +- Up to one balance-reserve or unreserve operation. +- One passthrough operation, one insert, both `O(S)` where `S` is the number of + signatories. `S` is capped by `MaxSignatories`, with weight being proportional. +- One encode & hash, both of complexity `O(S)`. +- Up to one binary search and insert (`O(logS + S)`). +- I/O: 1 read `O(S)`, up to 1 mutate `O(S)`. Up to one remove. +- One event. +- Storage: inserts one item, value size bounded by `MaxSignatories`, with a deposit + taken for its lifetime of `DepositBase + threshold * DepositFactor`. #### cancel_as_multi - 3 @@ -952,6 +1124,15 @@ dispatch. May not be empty. transaction for this dispatch. - `call_hash`: The hash of the call to be executed. +**Complexity** +- `O(S)`. +- Up to one balance-reserve or unreserve operation. +- One passthrough operation, one insert, both `O(S)` where `S` is the number of + signatories. `S` is capped by `MaxSignatories`, with weight being proportional. +- One encode & hash, both of complexity `O(S)`. +- One event. +- I/O: 1 read `O(S)`, one remove. +- Storage: removes one item. ### ProvideRandomness - 62 @@ -985,8 +1166,6 @@ call: Box<<T as Config>::RuntimeCall> Dispatch the given `call` from an account that the sender is authorised for through `add_proxy`. -Removes any corresponding announcement(s). - The dispatch origin for this call must be _Signed_. Parameters: @@ -1224,14 +1403,22 @@ calls: Vec<<T as Config>::RuntimeCall> Send a batch of dispatch calls. -May be called from any origin. +May be called from any origin except `None`. - `calls`: The calls to be dispatched from the same origin. The number of call must not exceed the constant: `batched_calls_limit` (available in constant metadata). -If origin is root then call are dispatch without checking origin filter. (This includes -bypassing `frame_system::Config::BaseCallFilter`). +If origin is root then the calls are dispatched without checking origin filter. (This +includes bypassing `frame_system::Config::BaseCallFilter`). + +**Complexity** +- O(C) where C is the number of calls to be batched. +This will return `Ok` in all circumstances. To determine the success of the batch, an +event is deposited. If a call failed and the batch was interrupted, then the +`BatchInterrupted` event is deposited, along with the number of successful calls made +and the error of the failed call. If all were successful, then the `BatchCompleted` +event is deposited. #### as_derivative - 1 @@ -1271,14 +1458,16 @@ calls: Vec<<T as Config>::RuntimeCall> Send a batch of dispatch calls and atomically execute them. The whole transaction will rollback and fail if any of the calls failed. -May be called from any origin. +May be called from any origin except `None`. - `calls`: The calls to be dispatched from the same origin. The number of call must not exceed the constant: `batched_calls_limit` (available in constant metadata). -If origin is root then call are dispatch without checking origin filter. (This includes -bypassing `frame_system::Config::BaseCallFilter`). +If origin is root then the calls are dispatched without checking origin filter. (This +includes bypassing `frame_system::Config::BaseCallFilter`). +**Complexity** +- O(C) where C is the number of calls to be batched. #### force_batch - 4 @@ -1293,14 +1482,34 @@ calls: Vec<<T as Config>::RuntimeCall> Send a batch of dispatch calls. Unlike `batch`, it allows errors and won't interrupt. -May be called from any origin. +May be called from any origin except `None`. - `calls`: The calls to be dispatched from the same origin. The number of call must not exceed the constant: `batched_calls_limit` (available in constant metadata). -If origin is root then call are dispatch without checking origin filter. (This includes -bypassing `frame_system::Config::BaseCallFilter`). +If origin is root then the calls are dispatch without checking origin filter. (This +includes bypassing `frame_system::Config::BaseCallFilter`). + +**Complexity** +- O(C) where C is the number of calls to be batched. + +#### with_weight - 5 + +<details><summary><code>with_weight(call, weight)</code></summary> +```rust +call: Box<<T as Config>::RuntimeCall> +weight: Weight +``` +</details> + + +Dispatch a function call with a specified weight. + +This function does not check the weight of the call, and instead allows the +Root origin to specify the weight of the call. + +The dispatch origin for this call must be _Root_. ### Treasury - 65 @@ -1319,6 +1528,8 @@ Put forward a suggestion for spending. A deposit proportional to the value is reserved and slashed if the proposal is rejected. It is returned once the proposal is awarded. +**Complexity** +- O(1) #### spend - 3 @@ -1356,28 +1567,23 @@ The original deposit will no longer be returned. May only be called from `T::RejectOrigin`. - `proposal_id`: The index of a proposal +**Complexity** +- O(A) where `A` is the number of approvals + +Errors: +- `ProposalNotApproved`: The `proposal_id` supplied was not found in the approval queue, +i.e., the proposal has not been approved. This could also mean the proposal does not +exist altogether, thus there is no way it would have been approved in the first place. ## Root calls -There are **22** root calls from **10** pallets. +There are **20** root calls from **10** pallets. ### System - 0 -#### fill_block - 0 - -<details><summary><code>fill_block(ratio)</code></summary> - -```rust -ratio: Perbill -``` -</details> - - -A dispatch that will fill the block weight up to the given ratio. - -#### set_heap_pages - 2 +#### set_heap_pages - 1 <details><summary><code>set_heap_pages(pages)</code></summary> @@ -1389,7 +1595,7 @@ pages: u64 Set the number of pages in the WebAssembly environment's heap. -#### set_code - 3 +#### set_code - 2 <details><summary><code>set_code(code)</code></summary> @@ -1401,8 +1607,10 @@ code: Vec<u8> Set the new runtime code. +**Complexity** +- `O(C + S)` where `C` length of `code` and `S` complexity of `can_set_code` -#### set_code_without_checks - 4 +#### set_code_without_checks - 3 <details><summary><code>set_code_without_checks(code)</code></summary> @@ -1414,8 +1622,10 @@ code: Vec<u8> Set the new runtime code without doing any checks of the given `code`. +**Complexity** +- `O(C)` where `C` length of `code` -#### set_storage - 5 +#### set_storage - 4 <details><summary><code>set_storage(items)</code></summary> @@ -1427,7 +1637,7 @@ items: Vec<KeyValue> Set some items of storage. -#### kill_storage - 6 +#### kill_storage - 5 <details><summary><code>kill_storage(keys)</code></summary> @@ -1439,7 +1649,7 @@ keys: Vec<Key> Kill some items from storage. -#### kill_prefix - 7 +#### kill_prefix - 6 <details><summary><code>kill_prefix(prefix, subkeys)</code></summary> @@ -1474,27 +1684,6 @@ not been enacted yet. ### Balances - 6 -#### set_balance - 1 - -<details><summary><code>set_balance(who, new_free, new_reserved)</code></summary> - -```rust -who: AccountIdLookupOf<T> -new_free: T::Balance -new_reserved: T::Balance -``` -</details> - - -Set the balances of a given account. - -This will alter `FreeBalance` and `ReservedBalance` in storage. it will -also alter the total issuance of the system (`TotalIssuance`) appropriately. -If the new free or reserved balance is below the existential deposit, -it will reset the account nonce (`frame_system::AccountNonce`). - -The dispatch origin for this call is `root`. - #### force_transfer - 2 <details><summary><code>force_transfer(source, dest, value)</code></summary> @@ -1507,8 +1696,8 @@ value: T::Balance </details> -Exactly as `transfer`, except the origin must be root and the source account may be -specified. +Exactly as `transfer_allow_death`, except the origin must be root and the source account +may be specified. #### force_unreserve - 5 @@ -1586,7 +1775,7 @@ Set the collective's membership. - `old_count`: The upper bound for the previous number of members in storage. Used for weight estimation. -Requires root origin. +The dispatch of this call must be `SetMembersOrigin`. NOTE: Does not enforce the expected `MaxMembers` limit on the amount of members, but the weight estimations rely on it to estimate dispatchable weight. @@ -1598,6 +1787,11 @@ implementation of the trait [`ChangeMembers`]. Any call to `set_members` must be careful that the member set doesn't get out of sync with other logic managing the member set. +**Complexity**: +- `O(MP + N)` where: + - `M` old-members-count (code- and governance-bounded) + - `N` new-members-count (code- and governance-bounded) + - `P` proposals-count (code-bounded) #### disapprove_proposal - 5 @@ -1617,16 +1811,19 @@ Must be called by the Root origin. Parameters: * `proposal_hash`: The hash of the proposal that should be disapproved. +**Complexity** +O(P) where P is the number of max proposals ### Identity - 41 #### remove_identity - 5 -<details><summary><code>remove_identity(idty_index, idty_name)</code></summary> +<details><summary><code>remove_identity(idty_index, idty_name, reason)</code></summary> ```rust idty_index: T::IdtyIndex idty_name: Option<IdtyName> +reason: IdtyRemovalReason<T::IdtyRemovalOtherReason> ``` </details> @@ -1716,6 +1913,8 @@ Dispatches a function call with a provided origin. The dispatch origin for this call must be _Root_. +**Complexity** +- O(1). @@ -1728,7 +1927,7 @@ There are **6** disabled calls from **3** pallets. ### System - 0 -#### remark - 1 +#### remark - 0 <details><summary><code>remark(remark)</code></summary> @@ -1740,8 +1939,10 @@ remark: Vec<u8> Make some on-chain remark. +**Complexity** +- `O(1)` -#### remark_with_event - 8 +#### remark_with_event - 7 <details><summary><code>remark_with_event(remark)</code></summary> @@ -1772,6 +1973,9 @@ This doesn't take effect until the next session. The dispatch origin of this function must be signed. +**Complexity** +- `O(1)`. Actual cost depends on the number of length of `T::Keys::key_ids()` which is + fixed. #### purge_keys - 1 @@ -1791,6 +1995,9 @@ convertible to a validator ID using the chain's typical addressing system (this means being a controller account) or directly convertible into a validator ID (which usually means being a stash account). +**Complexity** +- `O(1)` in number of key types. Actual cost depends on the number of length of + `T::Keys::key_ids()` which is fixed. ### Membership - 42 diff --git a/end2end-tests/cucumber-features/identity_creation.feature b/end2end-tests/cucumber-features/identity_creation.feature index 899ef935d..ce660e1ae 100644 --- a/end2end-tests/cucumber-features/identity_creation.feature +++ b/end2end-tests/cucumber-features/identity_creation.feature @@ -3,11 +3,11 @@ Feature: Identity creation Scenario: alice invites a new member to join the web of trust # 6 ÄžD covers: # - account creation fees (3 ÄžD) - # - existential deposit (2 ÄžD) + # - existential deposit (1 ÄžD) # - transaction fees (below 1 ÄžD) When alice sends 7 ÄžD to dave - # Alice is treasury funder for 1 ÄžD => 10-1-7 = 2 - Then alice should have 2 ÄžD + # Alice is treasury funder for 1 ÄžD => 10-1-7 = 2 (minus fees) + Then alice should have 199 cÄžD When bob sends 750 cÄžD to dave When charlie sends 6 ÄžD to eve # alice last certification is counted from block zero diff --git a/end2end-tests/cucumber-features/oneshot_account.feature b/end2end-tests/cucumber-features/oneshot_account.feature index 462847fa0..ebe5a19a1 100644 --- a/end2end-tests/cucumber-features/oneshot_account.feature +++ b/end2end-tests/cucumber-features/oneshot_account.feature @@ -2,22 +2,22 @@ Feature: Oneshot account Scenario: Simple oneshot consumption When alice sends 7 ÄžD to oneshot dave - # Alice is treasury funder for 1 ÄžD - Then alice should have 2 ÄžD + # Alice is treasury funder for 1 ÄžD and pays fees + Then alice should have 199 cÄžD Then dave should have oneshot 7 ÄžD When oneshot dave consumes into account bob Then dave should have oneshot 0 ÄžD - Then bob should have 1699 cÄžD + Then bob should have 1698 cÄžD Then bob should have oneshot 0 ÄžD Scenario: Double oneshot consumption When alice sends 7 ÄžD to oneshot dave - # Alice is treasury funder for 1 ÄžD - Then alice should have 2 ÄžD + # Alice is treasury funder for 1 ÄžD and pays fees + Then alice should have 199 cÄžD Then dave should have oneshot 7 ÄžD When oneshot dave consumes 4 ÄžD into account bob and the rest into oneshot charlie Then dave should have oneshot 0 ÄžD Then bob should have 14 ÄžD Then bob should have oneshot 0 ÄžD Then charlie should have 10 ÄžD - Then charlie should have oneshot 299 cÄžD + Then charlie should have oneshot 298 cÄžD diff --git a/end2end-tests/cucumber-features/transfer_all.feature b/end2end-tests/cucumber-features/transfer_all.feature index fafbc481d..8b276df61 100644 --- a/end2end-tests/cucumber-features/transfer_all.feature +++ b/end2end-tests/cucumber-features/transfer_all.feature @@ -6,9 +6,12 @@ Feature: Balance transfer all """ Bob is a member, as such he is not allowed to empty his account completely, if he tries to do so, the existence deposit (1 ÄžD) must remain. + Bob is a member, transaction fees are refunded for him + 101 = existential deposit (100) + fees refunded using quota (001) """ - Then bob should have 1 ÄžD + Then bob should have 101 cÄžD """ 10 ÄžD (initial Bob balance) - 1 ÄžD (Existential deposit) - 0.02 ÄžD (transaction fees) """ Then dave should have 898 cÄžD + # TODO check that the missing cent went to treasury diff --git a/end2end-tests/tests/common/mod.rs b/end2end-tests/tests/common/mod.rs index 41f3380cf..b147b9783 100644 --- a/end2end-tests/tests/common/mod.rs +++ b/end2end-tests/tests/common/mod.rs @@ -137,6 +137,8 @@ pub async fn spawn_node( "--tmp", // Fix: End2End test may fail due to network discovery. This option disables automatic peer discovery.Ï€ "--reserved-only", + // prevent local network discovery (even it does not connect due to above flag) + "--no-mdns", ], &duniter_binary_path, maybe_genesis_conf_file, diff --git a/live-tests/tests/sanity_gdev.rs b/live-tests/tests/sanity_gdev.rs index e0df612c5..766db91bd 100644 --- a/live-tests/tests/sanity_gdev.rs +++ b/live-tests/tests/sanity_gdev.rs @@ -38,7 +38,7 @@ type Index = u32; // Define gdev types type AccountInfo = gdev::runtime_types::frame_system::AccountInfo< Index, - gdev::runtime_types::pallet_duniter_account::types::AccountData<Balance>, + gdev::runtime_types::pallet_duniter_account::types::AccountData<Balance, IdtyIndex>, >; type IdtyData = gdev::runtime_types::common_runtime::entities::IdtyData; type IdtyIndex = u32; diff --git a/node/src/chain_spec/gdev.rs b/node/src/chain_spec/gdev.rs index f9d84c985..0a23f5b5b 100644 --- a/node/src/chain_spec/gdev.rs +++ b/node/src/chain_spec/gdev.rs @@ -24,7 +24,7 @@ use common_runtime::*; use gdev_runtime::{ opaque::SessionKeys, parameters, AccountConfig, AuthorityMembersConfig, BabeConfig, BalancesConfig, CertConfig, GenesisConfig, IdentityConfig, MembershipConfig, ParametersConfig, - SessionConfig, SmithCertConfig, SmithMembershipConfig, SudoConfig, SystemConfig, + QuotaConfig, SessionConfig, SmithCertConfig, SmithMembershipConfig, SudoConfig, SystemConfig, TechnicalCommitteeConfig, UniversalDividendConfig, WASM_BINARY, }; use jsonrpsee::core::JsonValue; @@ -302,6 +302,13 @@ fn genesis_data_to_gdev_genesis_conf( members: technical_committee_members, ..Default::default() }, + quota: QuotaConfig { + identities: identities + .iter() + .enumerate() + .map(|(i, _)| i as u32 + 1) + .collect(), + }, identity: IdentityConfig { identities: identities .into_iter() diff --git a/node/src/chain_spec/gen_genesis_data.rs b/node/src/chain_spec/gen_genesis_data.rs index d5296ce21..4d6ef39ed 100644 --- a/node/src/chain_spec/gen_genesis_data.rs +++ b/node/src/chain_spec/gen_genesis_data.rs @@ -64,7 +64,7 @@ type MembershipData = sp_membership::MembershipData<u32>; #[derive(Clone)] pub struct GenesisData<Parameters: DeserializeOwned, SessionKeys: Decode> { - pub accounts: BTreeMap<AccountId, GenesisAccountData<u64>>, + pub accounts: BTreeMap<AccountId, GenesisAccountData<u64, u32>>, pub treasury_balance: u64, pub certs_by_receiver: BTreeMap<u32, BTreeMap<u32, Option<u32>>>, pub first_ud: Option<u64>, @@ -237,7 +237,7 @@ struct SmithWoT<SK: Decode> { } struct GenesisInfo<'a> { - accounts: &'a BTreeMap<AccountId32, GenesisAccountData<u64>>, + accounts: &'a BTreeMap<AccountId32, GenesisAccountData<u64, u32>>, genesis_data_wallets_count: &'a usize, inactive_identities: &'a HashMap<u32, String>, identities: &'a Vec<GenesisIdentity>, @@ -888,13 +888,13 @@ fn v1_wallets_to_v2_accounts( ) -> ( bool, u64, - BTreeMap<AccountId32, GenesisAccountData<u64>>, + BTreeMap<AccountId32, GenesisAccountData<u64, u32>>, usize, ) { // monetary mass for double check let mut monetary_mass = 0u64; // account inserted in genesis - let mut accounts: BTreeMap<AccountId, GenesisAccountData<u64>> = BTreeMap::new(); + let mut accounts: BTreeMap<AccountId, GenesisAccountData<u64, u32>> = BTreeMap::new(); let mut invalid_wallets = 0; let mut fatal = false; for (pubkey, balance) in wallets { @@ -917,7 +917,7 @@ fn v1_wallets_to_v2_accounts( GenesisAccountData { random_id: H256(blake2_256(&(balance, &owner_key).encode())), balance, - is_identity: false, + idty_id: None, }, ); } else { @@ -1147,7 +1147,7 @@ fn make_authority_exist<SessionKeys: Encode, SKP: SessionKeysProvider<SessionKey } fn feed_identities( - accounts: &mut BTreeMap<AccountId32, GenesisAccountData<u64>>, + accounts: &mut BTreeMap<AccountId32, GenesisAccountData<u64, u32>>, identity_index: &mut HashMap<u32, String>, monetary_mass: &mut u64, inactive_identities: &mut HashMap<u32, String>, @@ -1196,7 +1196,7 @@ fn feed_identities( GenesisAccountData { random_id: H256(blake2_256(&(identity.index, &identity.owner_key).encode())), balance: identity.balance, - is_identity: true, + idty_id: Some(identity.index), }, ); @@ -1499,7 +1499,7 @@ where &(i as u32 + idty_index_start, owner_key).encode(), )), balance: ud, - is_identity: true, + idty_id: Some(i as u32 + idty_index_start), }, ) }) diff --git a/pallets/duniter-account/Cargo.toml b/pallets/duniter-account/Cargo.toml index f55acb242..71bb9afe7 100644 --- a/pallets/duniter-account/Cargo.toml +++ b/pallets/duniter-account/Cargo.toml @@ -32,6 +32,8 @@ try-runtime = ['frame-support/try-runtime'] [dependencies] # local +pallet-quota = { path = "../quota", default-features = false } +pallet-identity = { path = "../identity", default-features = false } pallet-provide-randomness = { path = "../provide-randomness", default-features = false } # crates.io @@ -61,6 +63,11 @@ default-features = false git = 'https://github.com/duniter/substrate' branch = 'duniter-substrate-v0.9.42' +[dependencies.pallet-transaction-payment] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.42' + [dependencies.pallet-treasury] default-features = false git = 'https://github.com/duniter/substrate' diff --git a/pallets/duniter-account/README.md b/pallets/duniter-account/README.md index d9b809d8a..5d9e73b14 100644 --- a/pallets/duniter-account/README.md +++ b/pallets/duniter-account/README.md @@ -1,6 +1,6 @@ # Duniter account pallet -Duniter customizes the `AccountData` of the `Balances` Substrate pallet. In particular, it adds a field `RandomId`. +Duniter customizes the `AccountData` of the `Balances` Substrate pallet. In particular, it adds the fields `random_id` and `linked_idty`. ## RandomID @@ -14,4 +14,8 @@ DuniterAccount defines a creation fee that is preleved to the account one block ## Sufficient -DuniterAccount tweaks the substrate AccountInfo to allow identity accounts to exist without existential deposit. This allows to spare the creation fee. \ No newline at end of file +DuniterAccount tweaks the substrate AccountInfo to allow identity accounts to exist without existential deposit. This allows to spare the creation fee. + +## Linked identity + +Duniter offers the possibility to link an account to an identity with the `linked_idty` field. It allows to request refund of transaction fees in `OnChargeTransaction`. \ No newline at end of file diff --git a/pallets/duniter-account/src/benchmarking.rs b/pallets/duniter-account/src/benchmarking.rs index f78891665..e0e300d0f 100644 --- a/pallets/duniter-account/src/benchmarking.rs +++ b/pallets/duniter-account/src/benchmarking.rs @@ -18,7 +18,7 @@ use super::*; -use frame_benchmarking::{benchmarks, whitelisted_caller}; +use frame_benchmarking::{account, benchmarks, whitelisted_caller}; use frame_support::sp_runtime::{traits::One, Saturating}; use frame_support::traits::{Currency, Get}; use pallet_provide_randomness::OnFilledRandomness; @@ -60,6 +60,10 @@ fn create_pending_accounts<T: Config>( } benchmarks! { + unlink_identity { + let account = account("Alice", 1, 1); + let origin = frame_system::RawOrigin::Signed(account); + }: _<T::RuntimeOrigin>(origin.into()) on_initialize_sufficient { let i in 0 .. T::MaxNewAccountsPerBlock::get() => create_pending_accounts::<T>(i, false, true)?; }: { Pallet::<T>::on_initialize(T::BlockNumber::one()); } diff --git a/pallets/duniter-account/src/lib.rs b/pallets/duniter-account/src/lib.rs index 908b768c8..56095dcbe 100644 --- a/pallets/duniter-account/src/lib.rs +++ b/pallets/duniter-account/src/lib.rs @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Affero General Public License // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. +// Note: refund queue mechanism is inspired from frame contract + #![cfg_attr(not(feature = "std"), no_std)] #[cfg(feature = "runtime-benchmarks")] @@ -27,16 +29,24 @@ pub use types::*; pub use weights::WeightInfo; use frame_support::pallet_prelude::*; +use frame_support::traits::{Currency, ExistenceRequirement, StorageVersion}; use frame_support::traits::{OnUnbalanced, StoredMap}; use frame_system::pallet_prelude::*; +use pallet_identity::IdtyEvent; use pallet_provide_randomness::RequestId; +use pallet_quota::traits::RefundFee; +use pallet_transaction_payment::OnChargeTransaction; use sp_core::H256; -use sp_runtime::traits::{Convert, Saturating}; +use sp_runtime::traits::{Convert, DispatchInfoOf, PostDispatchInfoOf, Saturating}; +use sp_std::fmt::Debug; #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::traits::{Currency, ExistenceRequirement, StorageVersion}; + pub type IdtyIdOf<T> = <T as pallet_identity::Config>::IdtyIndex; + pub type CurrencyOf<T> = pallet_balances::Pallet<T>; + pub type BalanceOf<T> = + <CurrencyOf<T> as Currency<<T as frame_system::Config>::AccountId>>::Balance; /// The current storage version. const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); @@ -50,10 +60,12 @@ pub mod pallet { #[pallet::config] pub trait Config: - frame_system::Config<AccountData = AccountData<Self::Balance>> + frame_system::Config<AccountData = AccountData<Self::Balance, IdtyIdOf<Self>>> + pallet_balances::Config + pallet_provide_randomness::Config<Currency = pallet_balances::Pallet<Self>> + + pallet_transaction_payment::Config + pallet_treasury::Config<Currency = pallet_balances::Pallet<Self>> + + pallet_quota::Config { type AccountIdToSalt: Convert<Self::AccountId, [u8; 32]>; #[pallet::constant] @@ -64,6 +76,10 @@ pub mod pallet { type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; /// Type representing the weight of this pallet type WeightInfo: WeightInfo; + /// wrapped type + type InnerOnChargeTransaction: OnChargeTransaction<Self>; + /// type implementing refund behavior + type Refund: pallet_quota::traits::RefundFee<Self>; } // STORAGE // @@ -82,8 +98,10 @@ pub mod pallet { #[pallet::genesis_config] pub struct GenesisConfig<T: Config> { - pub accounts: - sp_std::collections::btree_map::BTreeMap<T::AccountId, GenesisAccountData<T::Balance>>, + pub accounts: sp_std::collections::btree_map::BTreeMap< + T::AccountId, + GenesisAccountData<T::Balance, IdtyIdOf<T>>, + >, pub treasury_balance: T::Balance, } @@ -107,7 +125,6 @@ pub mod pallet { account.data.random_id = None; account.data.free = self.treasury_balance; account.providers = 1; - account.sufficients = 1; // the treasury will always be self-sufficient }, ); @@ -129,16 +146,19 @@ pub mod pallet { GenesisAccountData { random_id, balance, - is_identity, + idty_id, }, ) in &self.accounts { // if the balance is below existential deposit, the account must be an identity - assert!(balance >= &T::ExistentialDeposit::get() || *is_identity); + assert!(balance >= &T::ExistentialDeposit::get() || idty_id.is_some()); // mutate account frame_system::Account::<T>::mutate(account_id, |account| { account.data.random_id = Some(*random_id); account.data.free = *balance; + if idty_id.is_some() { + account.data.linked_idty = *idty_id; + } if balance >= &T::ExistentialDeposit::get() { // accounts above existential deposit self-provide account.providers = 1; @@ -166,11 +186,64 @@ pub mod pallet { /// Random id assigned /// [account_id, random_id] RandomIdAssigned { who: T::AccountId, random_id: H256 }, + /// account linked to identity + AccountLinked { + who: T::AccountId, + identity: IdtyIdOf<T>, + }, + /// account unlinked from identity + AccountUnlinked(T::AccountId), + } + + // CALLS // + #[pallet::call] + impl<T: Config> Pallet<T> { + /// unlink the identity associated with the account + #[pallet::call_index(0)] + #[pallet::weight(<T as pallet::Config>::WeightInfo::unlink_identity())] + pub fn unlink_identity(origin: OriginFor<T>) -> DispatchResultWithPostInfo { + let who = ensure_signed(origin)?; + Self::do_unlink_identity(who); + Ok(().into()) + } + } + + // INTERNAL FUNCTIONS // + impl<T: Config> Pallet<T> { + /// unlink account + pub fn do_unlink_identity(account_id: T::AccountId) { + // no-op if account already linked to nothing + frame_system::Account::<T>::mutate(&account_id, |account| { + if account.data.linked_idty.is_some() { + Self::deposit_event(Event::AccountUnlinked(account_id.clone())); + } + account.data.linked_idty = None; + }) + } + + /// link account to identity + pub fn do_link_identity(account_id: T::AccountId, idty_id: IdtyIdOf<T>) { + // no-op if identity does not change + if frame_system::Account::<T>::get(&account_id) + .data + .linked_idty + != Some(idty_id) + { + frame_system::Account::<T>::mutate(&account_id, |account| { + account.data.linked_idty = Some(idty_id); + Self::deposit_event(Event::AccountLinked { + who: account_id.clone(), + identity: idty_id, + }); + }) + } + } } // HOOKS // #[pallet::hooks] impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { + // on initialize, withdraw account creation tax fn on_initialize(_: T::BlockNumber) -> Weight { let mut total_weight = Weight::zero(); for account_id in PendingNewAccounts::<T>::iter_keys() @@ -251,6 +324,17 @@ pub mod pallet { } } +// implement account linker +impl<T> pallet_identity::traits::LinkIdty<T::AccountId, IdtyIdOf<T>> for Pallet<T> +where + T: Config, +{ + fn link_identity(account_id: T::AccountId, idty_id: IdtyIdOf<T>) { + Self::do_link_identity(account_id, idty_id); + } +} + +// implement on filled randomness impl<T> pallet_provide_randomness::OnFilledRandomness for Pallet<T> where T: Config, @@ -272,13 +356,14 @@ where } } +// implement accountdata storedmap impl<T, AccountId, Balance> frame_support::traits::StoredMap<AccountId, pallet_balances::AccountData<Balance>> for Pallet<T> where AccountId: Parameter + Member + MaybeSerializeDeserialize - + core::fmt::Debug + + Debug + sp_runtime::traits::MaybeDisplay + Ord + Into<[u8; 32]> @@ -290,11 +375,11 @@ where + Default + Copy + MaybeSerializeDeserialize - + core::fmt::Debug + + Debug + codec::MaxEncodedLen + scale_info::TypeInfo, T: Config - + frame_system::Config<AccountId = AccountId, AccountData = AccountData<Balance>> + + frame_system::Config<AccountId = AccountId, AccountData = AccountData<Balance, IdtyIdOf<T>>> + pallet_balances::Config<Balance = Balance> + pallet_provide_randomness::Config, { @@ -307,7 +392,7 @@ where f: impl FnOnce(&mut Option<pallet_balances::AccountData<Balance>>) -> Result<R, E>, ) -> Result<R, E> { let account = frame_system::Account::<T>::get(account_id); - let was_providing = account.data != T::AccountData::default(); + let was_providing = !account.data.free.is_zero() || !account.data.reserved.is_zero(); let mut some_data = if was_providing { Some(account.data.into()) } else { @@ -346,3 +431,71 @@ where Ok(result) } } + +// ------ +// allows pay fees with quota instead of currency if available +impl<T: Config> OnChargeTransaction<T> for Pallet<T> +where + T::InnerOnChargeTransaction: OnChargeTransaction< + T, + Balance = <CurrencyOf<T> as Currency<T::AccountId>>::Balance, + LiquidityInfo = Option<<CurrencyOf<T> as Currency<T::AccountId>>::NegativeImbalance>, + >, +{ + type Balance = BalanceOf<T>; + type LiquidityInfo = Option<<CurrencyOf<T> as Currency<T::AccountId>>::NegativeImbalance>; + + fn withdraw_fee( + who: &T::AccountId, + call: &T::RuntimeCall, + dispatch_info: &DispatchInfoOf<T::RuntimeCall>, + fee: Self::Balance, + tip: Self::Balance, + ) -> Result<Self::LiquidityInfo, TransactionValidityError> { + // does not change the withdraw fee step (still fallback to currency adapter or oneshot account) + T::InnerOnChargeTransaction::withdraw_fee(who, call, dispatch_info, fee, tip) + } + + fn correct_and_deposit_fee( + who: &T::AccountId, + dispatch_info: &DispatchInfoOf<T::RuntimeCall>, + post_info: &PostDispatchInfoOf<T::RuntimeCall>, + corrected_fee: Self::Balance, + tip: Self::Balance, + already_withdrawn: Self::LiquidityInfo, + ) -> Result<(), TransactionValidityError> { + // in any case, the default behavior is applied + T::InnerOnChargeTransaction::correct_and_deposit_fee( + who, + dispatch_info, + post_info, + corrected_fee, + tip, + already_withdrawn, + )?; + // if account can be exonerated, add it to a refund queue + let account_data = frame_system::Pallet::<T>::get(who); + if let Some(idty_index) = account_data.linked_idty { + T::Refund::request_refund(who.clone(), idty_index, corrected_fee.saturating_sub(tip)); + } + Ok(()) + } +} + +// implement identity event handler +impl<T: Config> pallet_identity::traits::OnIdtyChange<T> for Pallet<T> { + fn on_idty_change(idty_id: IdtyIdOf<T>, idty_event: &IdtyEvent<T>) -> Weight { + match idty_event { + // link account to newly created identity + IdtyEvent::Created { owner_key, .. } => { + Self::do_link_identity(owner_key.clone(), idty_id); + } + IdtyEvent::Confirmed + | IdtyEvent::Validated + | IdtyEvent::ChangedOwnerKey { .. } + | IdtyEvent::Removed { .. } => {} + } + // TODO proper weight + Weight::zero() + } +} diff --git a/pallets/duniter-account/src/types.rs b/pallets/duniter-account/src/types.rs index 09d9b795b..c9b9a02cf 100644 --- a/pallets/duniter-account/src/types.rs +++ b/pallets/duniter-account/src/types.rs @@ -21,8 +21,8 @@ use sp_core::H256; use sp_runtime::traits::Zero; // see `struct AccountData` for details in substrate code -#[derive(Clone, Decode, Default, Encode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] -pub struct AccountData<Balance> { +#[derive(Clone, Decode, Encode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] // Default, +pub struct AccountData<Balance, IdtyId> { /// A random identifier that can not be chosen by the user // this intends to be used as a robust identification system pub(super) random_id: Option<H256>, @@ -32,9 +32,26 @@ pub struct AccountData<Balance> { pub(super) reserved: Balance, // see Substrate AccountData fee_frozen: Balance, + /// an optional pointer to an identity + // used to know if this account is linked to a member + // used in quota system to refund fees + pub linked_idty: Option<IdtyId>, +} + +// explicit implementation of default trait (can not be derived) +impl<Balance: Zero, IdtyId> Default for AccountData<Balance, IdtyId> { + fn default() -> Self { + Self { + linked_idty: None, + random_id: None, + free: Balance::zero(), + reserved: Balance::zero(), + fee_frozen: Balance::zero(), + } + } } -impl<Balance: Zero> AccountData<Balance> { +impl<Balance: Zero, IdtyId> AccountData<Balance, IdtyId> { pub fn set_balances(&mut self, new_balances: pallet_balances::AccountData<Balance>) { self.free = new_balances.free; self.reserved = new_balances.reserved; @@ -44,8 +61,10 @@ impl<Balance: Zero> AccountData<Balance> { // convert Duniter AccountData to Balances AccountData // needed for trait implementation -impl<Balance: Zero> From<AccountData<Balance>> for pallet_balances::AccountData<Balance> { - fn from(account_data: AccountData<Balance>) -> Self { +impl<Balance: Zero, IdtyId> From<AccountData<Balance, IdtyId>> + for pallet_balances::AccountData<Balance> +{ + fn from(account_data: AccountData<Balance, IdtyId>) -> Self { Self { free: account_data.free, reserved: account_data.reserved, @@ -57,8 +76,8 @@ impl<Balance: Zero> From<AccountData<Balance>> for pallet_balances::AccountData< #[derive(Clone, Decode, Default, Encode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))] -pub struct GenesisAccountData<Balance> { +pub struct GenesisAccountData<Balance, IdtyId> { pub random_id: H256, pub balance: Balance, - pub is_identity: bool, + pub idty_id: Option<IdtyId>, } diff --git a/pallets/duniter-account/src/weights.rs b/pallets/duniter-account/src/weights.rs index cfdd68446..212eb7fe4 100644 --- a/pallets/duniter-account/src/weights.rs +++ b/pallets/duniter-account/src/weights.rs @@ -25,10 +25,22 @@ pub trait WeightInfo { fn on_initialize_no_balance(i: u32) -> Weight; fn on_filled_randomness_pending() -> Weight; fn on_filled_randomness_no_pending() -> Weight; + fn unlink_identity() -> Weight; } // Insecure weights implementation, use it for tests only! impl WeightInfo for () { + /// Storage: System Account (r:1 w:0) + /// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen) + fn unlink_identity() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3591` + // Minimum execution time: 95_130_000 picoseconds. + Weight::from_parts(110_501_000, 0) + .saturating_add(Weight::from_parts(0, 3591)) + .saturating_add(RocksDbWeight::get().reads(1)) + } // Storage: Account PendingNewAccounts (r:1 w:0) // Storage: ProvideRandomness RequestIdProvider (r:1 w:1) // Storage: ProvideRandomness RequestsIds (r:1 w:1) diff --git a/pallets/duniter-wot/src/lib.rs b/pallets/duniter-wot/src/lib.rs index f4e336c1e..1b3b9bdb6 100644 --- a/pallets/duniter-wot/src/lib.rs +++ b/pallets/duniter-wot/src/lib.rs @@ -107,8 +107,10 @@ pub mod pallet { #[pallet::error] pub enum Error<T, I = ()> { - /// Identity not allowed to claim membership - IdtyNotAllowedToClaimMembership, + /// Not enough certifications received to claim membership + NotEnoughCertsToClaimMembership, + /// Distance has not been evaluated positively + DistanceNotOK, /// Identity not allowed to request membership IdtyNotAllowedToRequestMembership, /// Identity not allowed to renew membership @@ -268,9 +270,12 @@ impl<T: Config<I>, I: 'static> sp_membership::traits::CheckMembershipCallAllowed fn check_idty_allowed_to_claim_membership(idty_index: &IdtyIndex) -> Result<(), DispatchError> { let idty_cert_meta = pallet_certification::Pallet::<T, I>::idty_cert_meta(idty_index); ensure!( - idty_cert_meta.received_count >= T::MinCertForMembership::get() - && T::IsDistanceOk::is_distance_ok(idty_index), - Error::<T, I>::IdtyNotAllowedToClaimMembership + idty_cert_meta.received_count >= T::MinCertForMembership::get(), + Error::<T, I>::NotEnoughCertsToClaimMembership + ); + ensure!( + T::IsDistanceOk::is_distance_ok(idty_index), + Error::<T, I>::DistanceNotOK, ); Ok(()) } @@ -279,10 +284,13 @@ impl<T: Config<I>, I: 'static> sp_membership::traits::CheckMembershipCallAllowed fn check_idty_allowed_to_renew_membership(idty_index: &IdtyIndex) -> Result<(), DispatchError> { if let Some(idty_value) = pallet_identity::Pallet::<T>::identity(idty_index) { ensure!( - idty_value.status == IdtyStatus::Validated - && T::IsDistanceOk::is_distance_ok(idty_index), + idty_value.status == IdtyStatus::Validated, Error::<T, I>::IdtyNotAllowedToRenewMembership ); + ensure!( + T::IsDistanceOk::is_distance_ok(idty_index), + Error::<T, I>::DistanceNotOK, + ); } else { return Err(Error::<T, I>::IdtyNotFound.into()); } @@ -333,7 +341,7 @@ where impl<T: Config<I>, I: 'static> pallet_identity::traits::OnIdtyChange<T> for Pallet<T, I> { fn on_idty_change(idty_index: IdtyIndex, idty_event: &IdtyEvent<T>) -> Weight { match idty_event { - IdtyEvent::Created { creator } => { + IdtyEvent::Created { creator, .. } => { if let Err(e) = <pallet_certification::Pallet<T, I>>::do_add_cert_checked( *creator, idty_index, true, ) { diff --git a/pallets/duniter-wot/src/mock.rs b/pallets/duniter-wot/src/mock.rs index 7bdf69631..cd25f68a9 100644 --- a/pallets/duniter-wot/src/mock.rs +++ b/pallets/duniter-wot/src/mock.rs @@ -127,13 +127,12 @@ impl pallet_identity::Config for Test { type IdtyData = (); type IdtyNameValidator = IdtyNameValidatorTestImpl; type IdtyIndex = IdtyIndex; + type AccountLinker = (); type IdtyRemovalOtherReason = IdtyRemovalWotReason; - type NewOwnerKeySigner = UintAuthorityId; - type NewOwnerKeySignature = TestSignature; + type Signer = UintAuthorityId; + type Signature = TestSignature; type OnIdtyChange = DuniterWot; type RemoveIdentityConsumers = (); - type RevocationSigner = UintAuthorityId; - type RevocationSignature = TestSignature; type RuntimeEvent = RuntimeEvent; type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] diff --git a/pallets/duniter-wot/src/tests.rs b/pallets/duniter-wot/src/tests.rs index e45e39049..6cefede1f 100644 --- a/pallets/duniter-wot/src/tests.rs +++ b/pallets/duniter-wot/src/tests.rs @@ -21,8 +21,8 @@ use codec::Encode; use frame_support::instances::{Instance1, Instance2}; use frame_support::{assert_noop, assert_ok}; use pallet_identity::{ - IdtyName, IdtyStatus, NewOwnerKeyPayload, RevocationPayload, NEW_OWNER_KEY_PAYLOAD_PREFIX, - REVOCATION_PAYLOAD_PREFIX, + IdtyIndexAccountIdPayload, IdtyName, IdtyStatus, RevocationPayload, + NEW_OWNER_KEY_PAYLOAD_PREFIX, REVOCATION_PAYLOAD_PREFIX, }; use sp_runtime::testing::TestSignature; @@ -123,7 +123,7 @@ fn test_smith_member_cant_change_its_idty_address() { run_to_block(2); let genesis_hash = System::block_hash(0); - let new_key_payload = NewOwnerKeyPayload { + let new_key_payload = IdtyIndexAccountIdPayload { genesis_hash: &genesis_hash, idty_index: 3u32, old_owner_key: &3u64, @@ -495,7 +495,7 @@ fn test_certification_expire() { // Alice can not claim her membership because she does not have enough certifications assert_noop!( Membership::claim_membership(RuntimeOrigin::signed(1)), - pallet_duniter_wot::Error::<Test, Instance1>::IdtyNotAllowedToClaimMembership + pallet_duniter_wot::Error::<Test, Instance1>::NotEnoughCertsToClaimMembership ); // --- BLOCK 23 --- diff --git a/pallets/identity/src/benchmarking.rs b/pallets/identity/src/benchmarking.rs index b1ddf6b68..c3df1b542 100644 --- a/pallets/identity/src/benchmarking.rs +++ b/pallets/identity/src/benchmarking.rs @@ -107,8 +107,8 @@ fn create_identities<T: Config>(i: u32) -> Result<(), &'static str> { benchmarks! { where_clause { where - T::NewOwnerKeySignature: From<sp_core::sr25519::Signature>, - T::RevocationSignature: From<sp_core::sr25519::Signature>, + T::Signature: From<sp_core::sr25519::Signature>, + T::Signature: From<sp_core::sr25519::Signature>, T::AccountId: From<AccountId32>, T::IdtyIndex: From<u32>, } @@ -161,7 +161,7 @@ benchmarks! { // Change key a first time to add an old-old key let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero()); - let new_key_payload = NewOwnerKeyPayload { + let new_key_payload = IdtyIndexAccountIdPayload { genesis_hash: &genesis_hash, idty_index: account.index, old_owner_key: &account.key, @@ -176,7 +176,7 @@ benchmarks! { // The sufficients for the old_old key will drop to 0 during benchmark let caller_origin: <T as frame_system::Config>::RuntimeOrigin = RawOrigin::Signed(caller.clone()).into(); let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero()); - let new_key_payload = NewOwnerKeyPayload { + let new_key_payload = IdtyIndexAccountIdPayload { genesis_hash: &genesis_hash, idty_index: account.index, old_owner_key: &caller_public, @@ -199,7 +199,7 @@ benchmarks! { // Change key // The sufficients for the old key will drop to 0 during benchmark let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero()); - let new_key_payload = NewOwnerKeyPayload { + let new_key_payload = IdtyIndexAccountIdPayload { genesis_hash: &genesis_hash, idty_index: account.index, old_owner_key: &account.key, @@ -265,6 +265,17 @@ benchmarks! { assert!(sufficient < frame_system::Pallet::<T>::sufficients(&account.key), "Sufficient not incremented"); } + link_account { + let alice_origin = RawOrigin::Signed(Identities::<T>::get(T::IdtyIndex::one()).unwrap().owner_key); + let bob_public = sr25519_generate(0.into(), None); + let bob: T::AccountId = MultiSigner::Sr25519(bob_public).into_account().into(); + let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero()); + let payload = ( + LINK_IDTY_PAYLOAD_PREFIX, genesis_hash, T::IdtyIndex::one(), bob.clone(), + ).encode(); + let signature = sr25519_sign(0.into(), &bob_public, &payload).unwrap().into(); + }: _<T::RuntimeOrigin>(alice_origin.into(), bob, signature) + impl_benchmark_test_suite!( Pallet, // Create genesis identity Alice to test benchmark in mock diff --git a/pallets/identity/src/lib.rs b/pallets/identity/src/lib.rs index ed40d67c4..8c96cc6a9 100644 --- a/pallets/identity/src/lib.rs +++ b/pallets/identity/src/lib.rs @@ -41,8 +41,12 @@ use sp_runtime::traits::{AtLeast32BitUnsigned, IdentifyAccount, One, Saturating, use sp_std::fmt::Debug; use sp_std::prelude::*; +// icok = identity change owner key pub const NEW_OWNER_KEY_PAYLOAD_PREFIX: [u8; 4] = [b'i', b'c', b'o', b'k']; +// revo = revocation pub const REVOCATION_PAYLOAD_PREFIX: [u8; 4] = [b'r', b'e', b'v', b'o']; +// link = link (identity with account) +pub const LINK_IDTY_PAYLOAD_PREFIX: [u8; 4] = [b'l', b'i', b'n', b'k']; #[frame_support::pallet] pub mod pallet { @@ -93,23 +97,21 @@ pub mod pallet { + MaybeSerializeDeserialize + Debug + MaxEncodedLen; + /// custom type for account data + type AccountLinker: LinkIdty<Self::AccountId, Self::IdtyIndex>; /// Handle logic to validate an identity name type IdtyNameValidator: IdtyNameValidator; /// Additional reasons for identity removal type IdtyRemovalOtherReason: Clone + Codec + Debug + Eq + TypeInfo; /// On identity confirmed by its owner type OnIdtyChange: OnIdtyChange<Self>; - /// Signing key of new owner key payload - type NewOwnerKeySigner: IdentifyAccount<AccountId = Self::AccountId>; - /// Signature of new owner key payload - type NewOwnerKeySignature: Parameter + Verify<Signer = Self::NewOwnerKeySigner>; + /// Signing key of a payload + type Signer: IdentifyAccount<AccountId = Self::AccountId>; + /// Signature of a payload + type Signature: Parameter + Verify<Signer = Self::Signer>; /// Handle the logic that removes all identity consumers. /// "identity consumers" meaning all things that rely on the existence of the identity. type RemoveIdentityConsumers: RemoveIdentityConsumers<Self::IdtyIndex>; - /// Signing key of revocation payload - type RevocationSigner: IdentifyAccount<AccountId = Self::AccountId>; - /// Signature of revocation payload - type RevocationSignature: Parameter + Verify<Signer = Self::RevocationSigner>; /// Because this pallet emits events, it depends on the runtime's definition of an event. type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; /// Type representing the weight of this pallet @@ -331,9 +333,9 @@ pub mod pallet { IdentityIndexOf::<T>::insert(owner_key.clone(), idty_index); Self::deposit_event(Event::IdtyCreated { idty_index, - owner_key, + owner_key: owner_key.clone(), }); - T::OnIdtyChange::on_idty_change(idty_index, &IdtyEvent::Created { creator }); + T::OnIdtyChange::on_idty_change(idty_index, &IdtyEvent::Created { creator, owner_key }); Ok(().into()) } @@ -418,7 +420,7 @@ pub mod pallet { /// Change identity owner key. /// /// - `new_key`: the new owner key. - /// - `new_key_sig`: the signature of the encoded form of `NewOwnerKeyPayload`. + /// - `new_key_sig`: the signature of the encoded form of `IdtyIndexAccountIdPayload`. /// Must be signed by `new_key`. /// /// The origin should be the old identity owner key. @@ -427,7 +429,7 @@ pub mod pallet { pub fn change_owner_key( origin: OriginFor<T>, new_key: T::AccountId, - new_key_sig: T::NewOwnerKeySignature, + new_key_sig: T::Signature, ) -> DispatchResultWithPostInfo { // verification phase let who = ensure_signed(origin)?; @@ -461,7 +463,7 @@ pub mod pallet { }; let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero()); - let new_key_payload = NewOwnerKeyPayload { + let new_key_payload = IdtyIndexAccountIdPayload { genesis_hash: &genesis_hash, idty_index, old_owner_key: &idty_value.owner_key, @@ -470,7 +472,7 @@ pub mod pallet { ensure!( (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload) .using_encoded(|bytes| new_key_sig.verify(bytes, &new_key)), - Error::<T>::InvalidNewOwnerKeySig + Error::<T>::InvalidSignature ); // Apply phase @@ -512,7 +514,7 @@ pub mod pallet { origin: OriginFor<T>, idty_index: T::IdtyIndex, revocation_key: T::AccountId, - revocation_sig: T::RevocationSignature, + revocation_sig: T::Signature, ) -> DispatchResultWithPostInfo { let _ = ensure_signed(origin)?; @@ -544,7 +546,7 @@ pub mod pallet { ensure!( (REVOCATION_PAYLOAD_PREFIX, revocation_payload) .using_encoded(|bytes| revocation_sig.verify(bytes, &revocation_key)), - Error::<T>::InvalidRevocationSig + Error::<T>::InvalidSignature ); // finally if all checks pass, remove identity @@ -605,6 +607,39 @@ pub mod pallet { Ok(().into()) } + + /// Link an account to an identity + // both must sign (target account and identity) + // can be used for quota system + // re-uses new owner key payload for simplicity + // with other custom prefix + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::link_account())] + pub fn link_account( + origin: OriginFor<T>, // origin must have an identity index + account_id: T::AccountId, // id of account to link (must sign the payload) + payload_sig: T::Signature, // signature with linked identity + ) -> DispatchResultWithPostInfo { + // verif + let who = ensure_signed(origin)?; + let idty_index = + IdentityIndexOf::<T>::get(&who).ok_or(Error::<T>::IdtyIndexNotFound)?; + let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero()); + let payload = IdtyIndexAccountIdPayload { + genesis_hash: &genesis_hash, + idty_index, + old_owner_key: &account_id, + }; + ensure!( + (LINK_IDTY_PAYLOAD_PREFIX, payload) + .using_encoded(|bytes| payload_sig.verify(bytes, &account_id)), + Error::<T>::InvalidSignature + ); + // apply + Self::do_link_account(account_id, idty_index); + + Ok(().into()) + } } // ERRORS // @@ -635,12 +670,10 @@ pub mod pallet { IdtyNotValidated, /// Identity not yet renewable IdtyNotYetRenewable, - /// New owner key payload signature is invalid - InvalidNewOwnerKeySig, + /// payload signature is invalid + InvalidSignature, /// Revocation key is invalid InvalidRevocationKey, - /// Revocation payload signature is invalid - InvalidRevocationSig, /// Identity creation period is not respected NotRespectIdtyCreationPeriod, /// Not the same identity name @@ -733,6 +766,12 @@ pub mod pallet { total_weight } + + /// link account + fn do_link_account(account_id: T::AccountId, idty_index: T::IdtyIndex) { + // call account linker + T::AccountLinker::link_identity(account_id, idty_index); + } } } @@ -762,7 +801,7 @@ where Default::default() } } - /// mutate an account fiven a function of its data + /// mutate an account given a function of its data fn try_mutate_exists<R, E: From<sp_runtime::DispatchError>>( key: &T::AccountId, f: impl FnOnce(&mut Option<T::IdtyData>) -> Result<R, E>, diff --git a/pallets/identity/src/mock.rs b/pallets/identity/src/mock.rs index 0238aa008..8311e4aa6 100644 --- a/pallets/identity/src/mock.rs +++ b/pallets/identity/src/mock.rs @@ -108,13 +108,12 @@ impl pallet_identity::Config for Test { type IdtyData = (); type IdtyNameValidator = IdtyNameValidatorTestImpl; type IdtyIndex = u64; + type AccountLinker = (); type IdtyRemovalOtherReason = (); - type NewOwnerKeySigner = AccountPublic; - type NewOwnerKeySignature = Signature; + type Signer = AccountPublic; + type Signature = Signature; type OnIdtyChange = (); type RemoveIdentityConsumers = (); - type RevocationSigner = AccountPublic; - type RevocationSignature = Signature; type RuntimeEvent = RuntimeEvent; type WeightInfo = (); #[cfg(feature = "runtime-benchmarks")] diff --git a/pallets/identity/src/tests.rs b/pallets/identity/src/tests.rs index c6b3df952..f112fa173 100644 --- a/pallets/identity/src/tests.rs +++ b/pallets/identity/src/tests.rs @@ -16,8 +16,9 @@ use crate::mock::*; use crate::{ - pallet, Error, GenesisIdty, IdtyName, IdtyRemovalReason, IdtyValue, NewOwnerKeyPayload, - RevocationPayload, NEW_OWNER_KEY_PAYLOAD_PREFIX, REVOCATION_PAYLOAD_PREFIX, + pallet, Error, GenesisIdty, IdtyIndexAccountIdPayload, IdtyName, IdtyRemovalReason, IdtyValue, + RevocationPayload, LINK_IDTY_PAYLOAD_PREFIX, NEW_OWNER_KEY_PAYLOAD_PREFIX, + REVOCATION_PAYLOAD_PREFIX, }; use codec::Encode; use frame_support::dispatch::DispatchResultWithPostInfo; @@ -158,7 +159,7 @@ fn test_create_identity_but_not_confirm_it() { System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyRemoved { idty_index: 2, - reason: crate::IdtyRemovalReason::<()>::Expired, + reason: IdtyRemovalReason::<()>::Expired, })); // We shoud be able to recreate the identity @@ -227,7 +228,7 @@ fn test_change_owner_key() { .execute_with(|| { let genesis_hash = System::block_hash(0); let old_owner_key = account(1).id; - let mut new_key_payload = NewOwnerKeyPayload { + let mut new_key_payload = IdtyIndexAccountIdPayload { genesis_hash: &genesis_hash, idty_index: 1u64, old_owner_key: &old_owner_key, @@ -261,7 +262,7 @@ fn test_change_owner_key() { (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload.clone()).encode() ) ), - Error::<Test>::InvalidNewOwnerKeySig + Error::<Test>::InvalidSignature ); // Payload must be prefixed @@ -271,7 +272,7 @@ fn test_change_owner_key() { account(10).id, test_signature(account(10).signer, new_key_payload.clone().encode()) ), - Error::<Test>::InvalidNewOwnerKeySig + Error::<Test>::InvalidSignature ); // New owner key should not be used by another identity @@ -370,6 +371,70 @@ fn test_change_owner_key() { }); } +// test link identity (does nothing because of AccountLinker type) +#[test] +fn test_link_account() { + new_test_ext(IdentityConfig { + identities: vec![alice(), bob()], + }) + .execute_with(|| { + let genesis_hash = System::block_hash(0); + let account_id = account(10).id; + let payload = IdtyIndexAccountIdPayload { + genesis_hash: &genesis_hash, + idty_index: 1u64, + old_owner_key: &account_id, + }; + + run_to_block(1); + + // Caller should have an associated identity + assert_noop!( + Identity::link_account( + RuntimeOrigin::signed(account(42).id), + account(10).id, + test_signature( + account(10).signer, + (LINK_IDTY_PAYLOAD_PREFIX, payload.clone()).encode() + ) + ), + Error::<Test>::IdtyIndexNotFound + ); + // Payload must be signed by the new key + assert_noop!( + Identity::link_account( + RuntimeOrigin::signed(account(1).id), + account(10).id, + test_signature( + account(42).signer, + (LINK_IDTY_PAYLOAD_PREFIX, payload.clone()).encode() + ) + ), + Error::<Test>::InvalidSignature + ); + + // Payload must be prefixed + assert_noop!( + Identity::link_account( + RuntimeOrigin::signed(account(1).id), + account(10).id, + test_signature(account(10).signer, payload.clone().encode()) + ), + Error::<Test>::InvalidSignature + ); + + // Alice can call link_account successfully + assert_ok!(Identity::link_account( + RuntimeOrigin::signed(account(1).id), + account(10).id, + test_signature( + account(10).signer, + (LINK_IDTY_PAYLOAD_PREFIX, payload.clone()).encode() + ) + )); + }); +} + #[test] fn test_idty_revocation_with_old_key() { new_test_ext(IdentityConfig { @@ -377,7 +442,7 @@ fn test_idty_revocation_with_old_key() { }) .execute_with(|| { let genesis_hash = System::block_hash(0); - let new_key_payload = NewOwnerKeyPayload { + let new_key_payload = IdtyIndexAccountIdPayload { genesis_hash: &genesis_hash, idty_index: 1u64, old_owner_key: &account(1).id, @@ -427,7 +492,7 @@ fn test_idty_revocation_with_old_key_after_old_key_expiration() { }) .execute_with(|| { let genesis_hash = System::block_hash(0); - let new_key_payload = NewOwnerKeyPayload { + let new_key_payload = IdtyIndexAccountIdPayload { genesis_hash: &genesis_hash, idty_index: 1u64, old_owner_key: &account(1).id, @@ -507,7 +572,7 @@ fn test_idty_revocation() { account(1).id, test_signature(account(1).signer, revocation_payload.encode()) ), - Err(Error::<Test>::InvalidRevocationSig.into()) + Err(Error::<Test>::InvalidSignature.into()) ); // Anyone can submit a revocation payload @@ -526,7 +591,7 @@ fn test_idty_revocation() { })); System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyRemoved { idty_index: 1, - reason: crate::IdtyRemovalReason::<()>::Revoked, + reason: IdtyRemovalReason::<()>::Revoked, })); run_to_block(2); diff --git a/pallets/identity/src/traits.rs b/pallets/identity/src/traits.rs index 1bd9a2a4c..86acc604b 100644 --- a/pallets/identity/src/traits.rs +++ b/pallets/identity/src/traits.rs @@ -77,6 +77,15 @@ impl<IndtyIndex> RemoveIdentityConsumers<IndtyIndex> for () { } } +/// trait used to link an account to an identity +pub trait LinkIdty<AccountId, IdtyIndex> { + fn link_identity(account_id: AccountId, idty_index: IdtyIndex); +} +impl<AccountId, IdtyIndex> LinkIdty<AccountId, IdtyIndex> for () { + fn link_identity(_: AccountId, _: IdtyIndex) {} +} + +/// trait used only in benchmarks to prepare identity for benchmarking #[cfg(feature = "runtime-benchmarks")] pub trait SetupBenchmark<IndtyIndex, AccountId> { fn force_status_ok(idty_index: &IndtyIndex, account: &AccountId) -> (); diff --git a/pallets/identity/src/types.rs b/pallets/identity/src/types.rs index 786aae6ab..f497ac34d 100644 --- a/pallets/identity/src/types.rs +++ b/pallets/identity/src/types.rs @@ -26,7 +26,10 @@ use sp_std::vec::Vec; /// events related to identity pub enum IdtyEvent<T: crate::Config> { /// creation of a new identity by an other - Created { creator: T::IdtyIndex }, + Created { + creator: T::IdtyIndex, + owner_key: T::AccountId, + }, /// confirmation of an identity (with a given name) Confirmed, /// validation of an identity @@ -121,9 +124,9 @@ pub struct IdtyValue<BlockNumber, AccountId, IdtyData> { /// payload to define a new owner key #[derive(Clone, Copy, Encode, RuntimeDebug)] -pub struct NewOwnerKeyPayload<'a, AccountId, IdtyIndex, Hash> { +pub struct IdtyIndexAccountIdPayload<'a, AccountId, IdtyIndex, Hash> { /// hash of the genesis block - // Avoid replay attack between networks + // Avoid replay attack across networks pub genesis_hash: &'a Hash, /// identity index pub idty_index: IdtyIndex, @@ -134,7 +137,7 @@ pub struct NewOwnerKeyPayload<'a, AccountId, IdtyIndex, Hash> { #[derive(Clone, Copy, Encode, Decode, PartialEq, Eq, TypeInfo, RuntimeDebug)] pub struct RevocationPayload<IdtyIndex, Hash> { /// hash of the genesis block - // Avoid replay attack between networks + // Avoid replay attack across networks pub genesis_hash: Hash, /// identity index pub idty_index: IdtyIndex, diff --git a/pallets/identity/src/weights.rs b/pallets/identity/src/weights.rs index 0f1246f69..f1058b770 100644 --- a/pallets/identity/src/weights.rs +++ b/pallets/identity/src/weights.rs @@ -28,6 +28,7 @@ pub trait WeightInfo { fn remove_identity() -> Weight; fn prune_item_identities_names(i: u32) -> Weight; fn fix_sufficients() -> Weight; + fn link_account() -> Weight; } // Insecure weights implementation, use it for tests only! @@ -135,4 +136,20 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(1 as u64)) .saturating_add(RocksDbWeight::get().writes(1 as u64)) } + /// Storage: Identity IdentityIndexOf (r:1 w:0) + /// Proof Skipped: Identity IdentityIndexOf (max_values: None, max_size: None, mode: Measured) + /// Storage: System BlockHash (r:1 w:0) + /// Proof: System BlockHash (max_values: None, max_size: Some(44), added: 2519, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen) + fn link_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `359` + // Estimated: `3824` + // Minimum execution time: 543_046_000 picoseconds. + Weight::from_parts(544_513_000, 0) + .saturating_add(Weight::from_parts(0, 3824)) + .saturating_add(RocksDbWeight::get().reads(3)) + .saturating_add(RocksDbWeight::get().writes(1)) + } } diff --git a/pallets/membership/src/lib.rs b/pallets/membership/src/lib.rs index 73a7e10a8..1b7a2d07e 100644 --- a/pallets/membership/src/lib.rs +++ b/pallets/membership/src/lib.rs @@ -233,9 +233,11 @@ pub mod pallet { Self::do_request_membership(idty_id, metadata) } - /// claim pending membership to become actual memberhip - /// the requested membership must fullfill requirements - // for main wot claim_membership is called automatically when validating identity + /// claim membership + /// a pending membership should exist + /// it must fullfill the requirements (certs, distance) + /// for main wot claim_membership is called automatically when validating identity + /// for smith wot, it means joining the authority members #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::claim_membership())] pub fn claim_membership(origin: OriginFor<T>) -> DispatchResultWithPostInfo { diff --git a/pallets/quota/Cargo.toml b/pallets/quota/Cargo.toml new file mode 100644 index 000000000..2c96eed66 --- /dev/null +++ b/pallets/quota/Cargo.toml @@ -0,0 +1,77 @@ +[package] +authors = ['HugoTrentesaux <hugo@trentesaux.fr>'] +description = 'duniter pallet quota' +edition = "2021" +homepage = 'https://duniter.org' +license = 'AGPL-3.0' +name = 'pallet-quota' +readme = 'README.md' +repository = 'https://git.duniter.org/nodes/rust/duniter-v2s' +version = '3.0.0' + +[features] +default = ['std'] +runtime-benchmarks = ['frame-benchmarking/runtime-benchmarks'] +std = [ + 'codec/std', + 'frame-support/std', + 'frame-system/std', + 'frame-benchmarking/std', + 'sp-core/std', + 'sp-runtime/std', + 'sp-std/std', + 'pallet-identity/std', + 'pallet-balances/std', +] +try-runtime = ['frame-support/try-runtime'] + +[package.metadata.docs.rs] +targets = ['x86_64-unknown-linux-gnu'] + +[dependencies] +pallet-identity = { path = "../identity", default-features = false } + +# crates.io +codec = { package = 'parity-scale-codec', version = "3.1.5", features = ['derive'], default-features = false } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } + +# substrate + +[dependencies.pallet-balances] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.42' + +[dependencies.frame-benchmarking] +default-features = false +git = 'https://github.com/duniter/substrate' +optional = true +branch = 'duniter-substrate-v0.9.42' + +[dependencies.frame-support] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.42' + +[dependencies.frame-system] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.42' + +[dependencies.sp-core] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.42' + +[dependencies.sp-runtime] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.42' + +[dependencies.sp-std] +default-features = false +git = 'https://github.com/duniter/substrate' +branch = 'duniter-substrate-v0.9.42' + +[dev-dependencies] +sp-io = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.42' } diff --git a/pallets/quota/README.md b/pallets/quota/README.md new file mode 100644 index 000000000..6ed526208 --- /dev/null +++ b/pallets/quota/README.md @@ -0,0 +1,31 @@ +# Duniter quota pallet + +Duniter identity system allows to allocate quota and refund transaction fees when not consumed. + +## General behavior + +Quota system is plugged to transactions fees which is a rather critical aspect of substrate. +That's why in `duniter-account` pallet `OnChargeTransaction` implementation, the default behavior is preserved, and refunds are added to a queue handeled in `on_idle`. + +## Path for a refund + +This is what happens on a transaction: + +- `frame-executive` calls `OnChargeTransaction` implementations +- `duniter-account` `OnChargeTransaction` implementation is called, and if an identity is linked to the account who pays the fees, `request_refund` is called +- `request_refund` implementation of `quota` pallet determines whether the fees are eligible for refund based on the identity and then call `queue_refund` +- `queue_refund` adds a refund to the `RefundQueue` which will be processed in `on_idle` +- during `on_idle`, `quota` pallet processes the refund queue within the supplied weight limit with `process_refund_queue` +- for each refund in the `RefundQueue`, `try_refund` is called +- it first tries to use quotas to refund fees with `spend_quota` +- if a certain amount of quotas has been spend, it actually performs the refund with `do_refund`, taking currency from the `RefundAccount` to give it back to the account who paid the fee + +The conditions for a refund to happen are: + +1. an identity is linked to the account who pays the fees +1. some quotas are defined for the identity and have a non-null value after update + + +## TODO + +- [ ] sanity test checking that only member identities have quota \ No newline at end of file diff --git a/pallets/quota/src/benchmarking.rs b/pallets/quota/src/benchmarking.rs new file mode 100644 index 000000000..587ff9cc5 --- /dev/null +++ b/pallets/quota/src/benchmarking.rs @@ -0,0 +1,64 @@ +// Copyright 2021-2023 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use frame_benchmarking::{account, benchmarks}; + +// FIXME this is a naïve implementation of benchmarks: +// - without properly prepare data +// - without "verify" blocks +// - without thinking about worst case scenario +// - without writing complexity in the term of refund queue length +// It's there as a seed for benchmark implementation and to use WeightInfo where needed. + +benchmarks! { + where_clause { + where + IdtyId<T>: From<u32>, + BalanceOf<T>: From<u64>, + } + queue_refund { + let account: T::AccountId = account("Alice", 1, 1); + let refund = Refund { + account, + identity: 1u32.into(), + amount: 10u64.into(), + }; + }: { Pallet::<T>::queue_refund(refund) } + spend_quota { + let idty_id = 1u32; + let amount = 1u64; + }: { Pallet::<T>::spend_quota(idty_id.into(), amount.into()) } + try_refund { + let account: T::AccountId = account("Alice", 1, 1); + let refund = Refund { + account, + identity: 1u32.into(), + amount: 10u64.into(), + }; + }: { Pallet::<T>::try_refund(refund) } + do_refund { + let account: T::AccountId = account("Alice", 1, 1); + let refund = Refund { + account, + identity: 1u32.into(), + amount: 10u64.into(), + }; + let amount = 5u64.into(); + }: { Pallet::<T>::do_refund(refund, amount) } +} diff --git a/pallets/quota/src/lib.rs b/pallets/quota/src/lib.rs new file mode 100644 index 000000000..eff611f21 --- /dev/null +++ b/pallets/quota/src/lib.rs @@ -0,0 +1,361 @@ +// Copyright 2021-2023 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod traits; +pub mod weights; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarking; + +use crate::traits::*; +use frame_support::pallet_prelude::*; +use frame_support::traits::{Currency, ExistenceRequirement}; +use frame_system::pallet_prelude::*; +pub use pallet::*; +use pallet_identity::IdtyEvent; +use sp_runtime::traits::Zero; +use sp_std::fmt::Debug; +pub use weights::WeightInfo; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + pub const MAX_QUEUED_REFUNDS: u32 = 256; + + // Currency used for quota is the one of pallet balances + pub type CurrencyOf<T> = pallet_balances::Pallet<T>; + // Balance used for quota is the one associated to balance currency + pub type BalanceOf<T> = + <CurrencyOf<T> as Currency<<T as frame_system::Config>::AccountId>>::Balance; + // identity id is pallet identity idty_index + pub type IdtyId<T> = <T as pallet_identity::Config>::IdtyIndex; + + #[pallet::pallet] + pub struct Pallet<T>(_); + + // CONFIG // + #[pallet::config] + pub trait Config: + frame_system::Config + pallet_balances::Config + pallet_identity::Config + { + /// Because this pallet emits events, it depends on the runtime's definition of an event. + type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; + /// number of blocks in which max quota is replenished + type ReloadRate: Get<Self::BlockNumber>; + /// maximum amount of quota an identity can get + type MaxQuota: Get<BalanceOf<Self>>; + /// Account used to refund fee + #[pallet::constant] + type RefundAccount: Get<Self::AccountId>; + /// Weight + type WeightInfo: WeightInfo; + } + + // TYPES // + #[derive(Encode, Decode, Clone, TypeInfo, Debug, PartialEq, MaxEncodedLen)] + pub struct Refund<AccountId, IdtyId, Balance> { + /// account to refund + pub account: AccountId, + /// identity to use quota + pub identity: IdtyId, + /// amount of refund + pub amount: Balance, + } + + #[derive(Encode, Decode, Clone, TypeInfo, Debug, PartialEq, MaxEncodedLen)] + pub struct Quota<BlockNumber, Balance> { + /// block number of last quota use + pub last_use: BlockNumber, + /// amount of remaining quota + pub amount: Balance, + } + + // STORAGE // + /// maps identity index to quota + #[pallet::storage] + #[pallet::getter(fn quota)] + pub type IdtyQuota<T: Config> = + StorageMap<_, Twox64Concat, IdtyId<T>, Quota<T::BlockNumber, BalanceOf<T>>, OptionQuery>; + + /// fees waiting for refund + #[pallet::storage] + pub type RefundQueue<T: Config> = StorageValue< + _, + BoundedVec<Refund<T::AccountId, IdtyId<T>, BalanceOf<T>>, ConstU32<MAX_QUEUED_REFUNDS>>, + ValueQuery, + >; + + // EVENTS // + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event<T: Config> { + /// Refunded fees to an account + Refunded { + who: T::AccountId, + identity: IdtyId<T>, + amount: BalanceOf<T>, + }, + // --- the following events let know that an error occured --- + /// No quota for identity + NoQuotaForIdty(IdtyId<T>), + /// No more currency available for refund + // should never happen if the fees are going to the refund account + NoMoreCurrencyForRefund, + /// Refund failed + // for example when account is destroyed + RefundFailed(T::AccountId), + /// Refund queue full + RefundQueueFull, + } + + // // ERRORS // + // #[pallet::error] + // pub enum Error<T> { + // // no errors in on_idle + // // instead events are emitted + // } + + // // CALLS // + // #[pallet::call] + // impl<T: Config> Pallet<T> { + // // no calls for this pallet, only automatic processing when idle + // } + + // INTERNAL FUNCTIONS // + impl<T: Config> Pallet<T> { + /// add a new refund to the queue + pub fn queue_refund(refund: Refund<T::AccountId, IdtyId<T>, BalanceOf<T>>) { + if RefundQueue::<T>::mutate(|v| v.try_push(refund)).is_err() { + Self::deposit_event(Event::RefundQueueFull); + } + } + + /// try to refund using quota if available + pub fn try_refund(queued_refund: Refund<T::AccountId, IdtyId<T>, BalanceOf<T>>) -> Weight { + // get the amount of quota that identity is able to spend + let amount = Self::spend_quota(queued_refund.identity, queued_refund.amount); + if amount.is_zero() { + // partial weight + return <T as pallet::Config>::WeightInfo::spend_quota(); + } + // only perform refund if amount is not null + Self::do_refund(queued_refund, amount); + // total weight + <T as pallet::Config>::WeightInfo::spend_quota() + .saturating_add(<T as pallet::Config>::WeightInfo::do_refund()) + } + + /// do refund a non-null amount + // opti: more accurate estimation of consumed weight + pub fn do_refund( + queued_refund: Refund<T::AccountId, IdtyId<T>, BalanceOf<T>>, + amount: BalanceOf<T>, + ) { + // take money from refund account + let res = CurrencyOf::<T>::withdraw( + &T::RefundAccount::get(), + amount, + frame_support::traits::WithdrawReasons::FEE, // a fee but in reverse + ExistenceRequirement::KeepAlive, + ); + // if successful + if let Ok(imbalance) = res { + // perform refund + let res = CurrencyOf::<T>::resolve_into_existing(&queued_refund.account, imbalance); + match res { + // take money from refund account OK + refund account OK → event + Ok(_) => { + Self::deposit_event(Event::Refunded { + who: queued_refund.account, + identity: queued_refund.identity, + amount, + }); + } + Err(imbalance) => { + // refund failed (for example account stopped existing) → handle dust + // give back to refund account (should not happen) + CurrencyOf::<T>::resolve_creating(&T::RefundAccount::get(), imbalance); + // if this event is observed, block should be examined carefully + Self::deposit_event(Event::RefundFailed(queued_refund.account)); + } + } + } else { + // could not withdraw refund account + Self::deposit_event(Event::NoMoreCurrencyForRefund); + } + } + + /// perform as many refunds as possible within the supplied weight limit + pub fn process_refund_queue(weight_limit: Weight) -> Weight { + RefundQueue::<T>::mutate(|queue| { + if queue.is_empty() { + return Weight::zero(); + } + let mut total_weight = Weight::zero(); + // make sure that we have at least the time to handle one try_refund call + while total_weight.any_lt( + weight_limit.saturating_sub(<T as pallet::Config>::WeightInfo::try_refund()), + ) { + let Some(queued_refund) = queue.pop() else { + break; + }; + let consumed_weight = Self::try_refund(queued_refund); + total_weight = total_weight.saturating_add(consumed_weight); + } + total_weight + }) + } + + /// spend quota of identity + pub fn spend_quota(idty_id: IdtyId<T>, amount: BalanceOf<T>) -> BalanceOf<T> { + IdtyQuota::<T>::mutate_exists(idty_id, |quota| { + if let Some(ref mut quota) = quota { + Self::update_quota(quota); + Self::do_spend_quota(quota, amount) + } else { + // error event if identity has no quota + Self::deposit_event(Event::NoQuotaForIdty(idty_id)); + BalanceOf::<T>::zero() + } + }) + } + + /// update quota according to the growth rate, max value, and last use + fn update_quota(quota: &mut Quota<T::BlockNumber, BalanceOf<T>>) { + let current_block = frame_system::pallet::Pallet::<T>::block_number(); + let quota_growth = sp_runtime::Perbill::from_rational( + current_block - quota.last_use, + T::ReloadRate::get(), + ) + .mul_floor(T::MaxQuota::get()); + // mutate quota + quota.last_use = current_block; + quota.amount = core::cmp::min(quota.amount + quota_growth, T::MaxQuota::get()); + } + /// spend a certain amount of quota and return what was spent + fn do_spend_quota( + quota: &mut Quota<T::BlockNumber, BalanceOf<T>>, + amount: BalanceOf<T>, + ) -> BalanceOf<T> { + let old_amount = quota.amount; + // entire amount fit in remaining quota + if amount <= old_amount { + quota.amount -= amount; + amount + } + // all quota are spent and only partial refund is possible + else { + quota.amount = BalanceOf::<T>::zero(); + old_amount + } + } + } + + // GENESIS STUFF // + #[pallet::genesis_config] + pub struct GenesisConfig<T: Config> { + pub identities: Vec<IdtyId<T>>, + } + + #[cfg(feature = "std")] + impl<T: Config> Default for GenesisConfig<T> { + fn default() -> Self { + Self { + identities: Default::default(), + } + } + } + + #[pallet::genesis_build] + impl<T: Config> GenesisBuild<T> for GenesisConfig<T> { + fn build(&self) { + for idty in self.identities.iter() { + IdtyQuota::<T>::insert( + idty, + Quota { + last_use: T::BlockNumber::zero(), + amount: BalanceOf::<T>::zero(), + }, + ); + } + } + } + + // HOOKS // + #[pallet::hooks] + impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> { + // process refund queue if space left on block + fn on_idle(_block: T::BlockNumber, remaining_weight: Weight) -> Weight { + Self::process_refund_queue(remaining_weight) + // opti: benchmark process_refund_queue overhead and substract this from weight limit + // .saturating_sub(T::WeightInfo::process_refund_queue()) + } + } +} + +// implement quota traits +impl<T: Config> RefundFee<T> for Pallet<T> { + fn request_refund(account: T::AccountId, identity: IdtyId<T>, amount: BalanceOf<T>) { + if is_eligible_for_refund::<T>(identity) { + Self::queue_refund(Refund { + account, + identity, + amount, + }) + } + } +} + +/// tells whether an identity is eligible for refund +fn is_eligible_for_refund<T: pallet_identity::Config>(_identity: IdtyId<T>) -> bool { + // all identities are eligible for refund, no matter their status + // if the identity has no quotas or has been deleted, the refund request is still queued + // but when handeled, no refund will be issued (and `NoQuotaForIdty` may be raised) + true +} + +// implement identity event handler +impl<T: Config> pallet_identity::traits::OnIdtyChange<T> for Pallet<T> { + fn on_idty_change(idty_id: IdtyId<T>, idty_event: &IdtyEvent<T>) -> Weight { + match idty_event { + // initialize quota on identity creation + IdtyEvent::Created { .. } => { + IdtyQuota::<T>::insert( + idty_id, + Quota { + last_use: frame_system::pallet::Pallet::<T>::block_number(), + amount: BalanceOf::<T>::zero(), + }, + ); + } + IdtyEvent::Removed { .. } => { + IdtyQuota::<T>::remove(idty_id); + } + IdtyEvent::Confirmed | IdtyEvent::Validated | IdtyEvent::ChangedOwnerKey { .. } => {} + } + // TODO proper weight + Weight::zero() + } +} diff --git a/pallets/quota/src/mock.rs b/pallets/quota/src/mock.rs new file mode 100644 index 000000000..17b825fc4 --- /dev/null +++ b/pallets/quota/src/mock.rs @@ -0,0 +1,189 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +// Note: most of this file is copy pasted from common pallet_config and other mocks + +use super::*; +pub use crate::pallet as pallet_quota; +use frame_support::{ + parameter_types, + traits::{Everything, OnFinalize, OnInitialize}, +}; +use frame_system as system; +use sp_core::{Pair, H256}; +use sp_runtime::traits::IdentifyAccount; +use sp_runtime::traits::Verify; +use sp_runtime::BuildStorage; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + MultiSignature, MultiSigner, +}; + +type BlockNumber = u64; +type Balance = u64; +type Block = frame_system::mocking::MockBlock<Test>; +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>; +pub type Signature = MultiSignature; +pub type AccountPublic = <Signature as Verify>::Signer; +pub type AccountId = <AccountPublic as IdentifyAccount>::AccountId; + +pub fn account(id: u8) -> AccountId { + let pair = sp_core::sr25519::Pair::from_seed(&[id; 32]); + MultiSigner::Sr25519(pair.public()).into_account() +} + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event<T>}, + Quota: pallet_quota::{Pallet, Storage, Config<T>, Event<T>}, + Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>}, + Identity: pallet_identity::{Pallet, Call, Storage, Config<T>, Event<T>}, + } +); + +// QUOTA // +pub struct TreasuryAccountId; +impl frame_support::pallet_prelude::Get<AccountId> for TreasuryAccountId { + fn get() -> AccountId { + account(99) + } +} +parameter_types! { + pub const ReloadRate: u64 = 10; + pub const MaxQuota: u64 = 1000; +} +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type ReloadRate = ReloadRate; + type MaxQuota = MaxQuota; + type RefundAccount = TreasuryAccountId; + type WeightInfo = (); +} + +// SYSTEM // +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} +impl system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup<Self::AccountId>; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData<Balance>; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +// BALANCES // +parameter_types! { + pub const ExistentialDeposit: Balance = 1000; + pub const MaxLocks: u32 = 50; +} +impl pallet_balances::Config for Test { + type Balance = Balance; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = pallet_balances::weights::SubstrateWeight<Test>; + type MaxLocks = MaxLocks; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type RuntimeEvent = RuntimeEvent; + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; +} + +// IDENTITY // +parameter_types! { + pub const ChangeOwnerKeyPeriod: u64 = 10; + pub const ConfirmPeriod: u64 = 2; + pub const IdtyCreationPeriod: u64 = 3; + pub const MaxInactivityPeriod: u64 = 5; + pub const ValidationPeriod: u64 = 2; +} +pub struct IdtyNameValidatorTestImpl; +impl pallet_identity::traits::IdtyNameValidator for IdtyNameValidatorTestImpl { + fn validate(idty_name: &pallet_identity::IdtyName) -> bool { + idty_name.0.len() < 16 + } +} +impl pallet_identity::Config for Test { + type ChangeOwnerKeyPeriod = ChangeOwnerKeyPeriod; + type ConfirmPeriod = ConfirmPeriod; + type CheckIdtyCallAllowed = (); + type IdtyCreationPeriod = IdtyCreationPeriod; + type IdtyData = (); + type IdtyNameValidator = IdtyNameValidatorTestImpl; + type IdtyIndex = u64; + type AccountLinker = (); + type IdtyRemovalOtherReason = (); + type Signer = AccountPublic; + type Signature = Signature; + type OnIdtyChange = (); + type RemoveIdentityConsumers = (); + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext(gen_conf: pallet_quota::GenesisConfig<Test>) -> sp_io::TestExternalities { + GenesisConfig { + system: SystemConfig::default(), + balances: BalancesConfig::default(), + quota: gen_conf, + identity: IdentityConfig::default(), + } + .build_storage() + .unwrap() + .into() +} + +pub fn run_to_block(n: BlockNumber) { + while System::block_number() < n { + <frame_system::Pallet<Test> as OnFinalize<BlockNumber>>::on_finalize(System::block_number()); + System::reset_events(); + System::set_block_number(System::block_number() + 1); + <frame_system::Pallet<Test> as OnInitialize<BlockNumber>>::on_initialize( + System::block_number(), + ); + } +} diff --git a/pallets/quota/src/tests.rs b/pallets/quota/src/tests.rs new file mode 100644 index 000000000..14fa890f7 --- /dev/null +++ b/pallets/quota/src/tests.rs @@ -0,0 +1,241 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +use crate::mock::*; +use crate::Weight; +use frame_support::traits::Currency; +use sp_core::Get; + +// Note: values for reload rate and max quota defined in mock file +// parameter_types! { +// pub const ReloadRate: u64 = 10; +// pub const MaxQuota: u64 = 1000; +// } +// pub const ExistentialDeposit: Balance = 1000; + +/// test that quota are well initialized for genesis identities +#[test] +fn test_initial_quota() { + new_test_ext(QuotaConfig { + identities: vec![1, 2, 3], + }) + .execute_with(|| { + run_to_block(1); + + // quota initialized to 0,0 for a given identity + assert_eq!( + Quota::quota(1), + Some(pallet_quota::Quota { + last_use: 0, + amount: 0 + }) + ); + // no initialized quota for standard account + assert_eq!(Quota::quota(4), None); + }) +} + +/// test that quota are updated according to the reload rate and max quota values +#[test] +fn test_update_quota() { + new_test_ext(QuotaConfig { + identities: vec![1, 2, 3], + }) + .execute_with(|| { + // Block 1 + run_to_block(1); + assert_eq!( + Quota::quota(1), + Some(pallet_quota::Quota { + last_use: 0, + amount: 0 + }) + ); + // (spending 0 quota will lead to only update) + // assert zero quota spent + assert_eq!(Quota::spend_quota(1, 0), 0); + assert_eq!( + Quota::quota(1), + Some(pallet_quota::Quota { + last_use: 1, // used at block 1 + // max quota × (current block - last use) / reload rate + amount: 100 // 1000 × 1 / 10 = 100 + }) + ); + + // Block 2 + run_to_block(2); + assert_eq!(Quota::spend_quota(2, 0), 0); + assert_eq!( + Quota::quota(2), + Some(pallet_quota::Quota { + last_use: 2, // used at block 2 + // max quota × (current block - last use) / reload rate + amount: 200 // 1000 × 2 / 10 = 200 + }) + ); + + // Block 20 + run_to_block(20); + assert_eq!(Quota::spend_quota(2, 0), 0); + assert_eq!( + Quota::quota(2), + Some(pallet_quota::Quota { + last_use: 20, // used at block 20 + // maximum quota is reached + // 1000 × (20 - 2) / 10 = 1800 + amount: 1000 // min(1000, 1800) + }) + ); + }) +} + +/// test that right amount of quota is spent +#[test] +fn test_spend_quota() { + new_test_ext(QuotaConfig { + identities: vec![1, 2, 3], + }) + .execute_with(|| { + // at block 5, quota are half loaded (500) + run_to_block(5); + // spending less than available + assert_eq!(Quota::spend_quota(1, 200), 200); + assert_eq!( + Quota::quota(1), + Some(pallet_quota::Quota { + last_use: 5, + amount: 300 // 500 - 200 + }) + ); + // spending all available + assert_eq!(Quota::spend_quota(2, 500), 500); + assert_eq!( + Quota::quota(2), + Some(pallet_quota::Quota { + last_use: 5, + amount: 0 // 500 - 500 + }) + ); + // spending more than available + assert_eq!(Quota::spend_quota(3, 1000), 500); + assert_eq!( + Quota::quota(3), + Some(pallet_quota::Quota { + last_use: 5, + amount: 0 // 500 - 500 + }) + ); + }) +} + +/// test complete scenario with queue and process refund queue +#[test] +fn test_process_refund_queue() { + new_test_ext(QuotaConfig { + identities: vec![1, 2], + }) + .execute_with(|| { + run_to_block(5); + // give enough currency to accounts and treasury and double check + Balances::make_free_balance_be(&account(1), 1000); + Balances::make_free_balance_be(&account(2), 1000); + Balances::make_free_balance_be(&account(3), 1000); + Balances::make_free_balance_be( + &<Test as pallet_quota::Config>::RefundAccount::get(), + 10_000, + ); + assert_eq!( + Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()), + 10_000 + ); + // fill in the refund queue + Quota::queue_refund(pallet_quota::Refund { + account: account(1), + identity: 1, + amount: 10, + }); + Quota::queue_refund(pallet_quota::Refund { + account: account(2), + identity: 2, + amount: 1000, + }); + Quota::queue_refund(pallet_quota::Refund { + account: account(3), + identity: 3, + amount: 666, + }); + // process it + Quota::process_refund_queue(Weight::from(10)); + // after processing, it should be empty + assert!(pallet_quota::RefundQueue::<Test>::get().is_empty()); + // and we should observe the effects of refund + assert_eq!(Balances::free_balance(account(1)), 1010); // 1000 initial + 10 refunded + assert_eq!(Balances::free_balance(account(2)), 1500); // 1000 initial + 500 refunded + assert_eq!(Balances::free_balance(account(3)), 1000); // only initial because no available quota + assert_eq!( + Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()), + // initial minus refunds + 10_000 - 500 - 10 + ); + // events + System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::Refunded { + who: account(1), + identity: 1, + amount: 10, + })); + System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::NoQuotaForIdty(3))); + }) +} + +/// test not enough currency in treasury +#[test] +fn test_not_enough_treasury() { + new_test_ext(QuotaConfig { + identities: vec![1], + }) + .execute_with(|| { + run_to_block(5); + Balances::make_free_balance_be(&account(1), 1000); + Balances::make_free_balance_be(&<Test as pallet_quota::Config>::RefundAccount::get(), 1200); + Quota::queue_refund(pallet_quota::Refund { + account: account(1), + identity: 1, + amount: 500, + }); + Quota::process_refund_queue(Weight::from(10)); + // refund was not possible, would kill treasury + assert_eq!(Balances::free_balance(account(1)), 1000); + assert_eq!( + Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()), + 1200 + ); + // event + System::assert_has_event(RuntimeEvent::Quota( + pallet_quota::Event::NoMoreCurrencyForRefund, + )); + // quotas were spent anyway, there is no refund for quotas when refund account is empty + assert_eq!( + Quota::quota(1), + Some(pallet_quota::Quota { + last_use: 5, + amount: 0 + }) + ); + }) +} + +// TODO implement a mock weight to test if refund queue processing actually stops when reached limit diff --git a/pallets/quota/src/traits.rs b/pallets/quota/src/traits.rs new file mode 100644 index 000000000..fb2e377fd --- /dev/null +++ b/pallets/quota/src/traits.rs @@ -0,0 +1,27 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +use crate::*; + +/// trait used to request refund of a fee +pub trait RefundFee<T: Config> { + /// request refund for the account `account` using the quotas of identity `identity` + fn request_refund(account: T::AccountId, identity: IdtyId<T>, amount: BalanceOf<T>); +} +// dummy impl +impl<T: Config> RefundFee<T> for () { + fn request_refund(_account: T::AccountId, _identity: IdtyId<T>, _amount: BalanceOf<T>) {} +} diff --git a/pallets/quota/src/weights.rs b/pallets/quota/src/weights.rs new file mode 100644 index 000000000..c6cadaf10 --- /dev/null +++ b/pallets/quota/src/weights.rs @@ -0,0 +1,25 @@ +// tmp + +use frame_support::weights::Weight; + +pub trait WeightInfo { + fn queue_refund() -> Weight; + fn spend_quota() -> Weight; + fn try_refund() -> Weight; + fn do_refund() -> Weight; +} + +impl WeightInfo for () { + fn queue_refund() -> Weight { + Weight::from_parts(999u64, 0) + } + fn spend_quota() -> Weight { + Weight::from_parts(999u64, 0) + } + fn try_refund() -> Weight { + Weight::from_parts(999u64, 0) + } + fn do_refund() -> Weight { + Weight::from_parts(999u64, 0) + } +} diff --git a/resources/metadata.scale b/resources/metadata.scale index fe56431a1e9d65f5bf7b826f845fb741cae101f6..2680c3b02a4ca74e19ed66cc13eb88883db14aef 100644 GIT binary patch delta 11875 zcmd^ldt6o3w*UIBy*6wVbfbV91zad7C@3f>AU;s>k@6BXEfKe{ke&O1`=M~k0Lw~` zmO9g!J&sy=lhV|?^K7lW$}Kxt+KF3MR+gNrx6-U*Cp&qLp6*y{Z621tKYoAyKEKZg z)}CuV#++-+F~|6hG3K=gOn=&93UMf2Yp?MKLn0Alikob;Tb)%3ES8zo-iDdgFbh#E z2@4XCX16()E7g@Ys!u{rNuAf`a^~y3Ca`-kpZ{ehl90mMke-z$gu~)=xgAz}8l<@b z(p+_l+o}%fs7Z2Ioz+&a%iW;$5YpHvaac+Q!hK`11f<ltU9Or+kL`C#uu8}fgb<66 zB?zKP$YJk@1K1AHJRl$W^OdUnskUW`$2-%jI3@@}KJ}R-OrkzxFqOd&-9HO)YA;{T z1R+F7n&NVL6sO1MF$r38bhl<nFqbP<d$0vrtj-Wc&$WhOky8c1APItC5@yf@vxV90 zxFI38RGJH!J~yha*6L9f0#T)EK|qwUQgM1qAr%U+cwKe2Do+JqQP(k-7Fo*rN%1}F zEaeRzTD2t1WfP_0hI%})`TH2Q%oy1lLQ;8Ety1l?D{e`+k3A=4U=#aL%Ec3`mvIuF zW+leq6Qas|F0WM*_MooLYPT!i$`xuwUS6R(d;T1s(`$1m6NJgaqynF}*5$T&8;TUi zQpN3&guU!hV-8+ne=;WHb#}#=gyXDFNHRWTIU#BImMI|__>pa+^3UvzkX*wJ++c~J zeOaH-DUuO*o*m7OWh+7x5WzNuj$%$zK}hBVVVGcMubPV4ou+<BW-e11Qdz5MA=24^ zu+hk6C1JTz0noIy0<mm&SRc$}hr`lP#x8~p!8{h;V+iK5(LE+(0bAMQPE@n^dJMz@ z|93scgKZ9vWmCf~ebQjLQ?V_p^%9?}>{gFw5p#z34_yLtskOnw&W8_RFNWt}2}z@c z{WCm<sPu`LjapV2Q3^Xd5D|wub~+*nUiL%8WU9~ZHIigS={1yEH}={`t>bzZQ|tQP z6HveTiiJ&=`@rn~vG-u~$||U;auH)^YbS!i+^<o#RTI03w`Qd2XQHG=-DXj$wd$Q^ z{rt#YS7+b+Af0(my^Vwm8FTaUg4HvtN#5!^hWmn@<Qlij(Mdwug&tvHVJviEHSXiB zBQSvdA@W5uv7S+b@F27I8GuJvTY6viRG;+D+Hd=;>MTFp_kqsxh^QrDO@bhq%x2+1 z#-oxlACW9`6pzpDHIbBWkP1-^lc{96By6B_AcPmXNiwTx)q+|<%irYxHfo{)kNeXs zb3*VqYfPA7cp|)(nT`GZ7ZUCO3GGPvGIp{7iL>y$e_3KY7VcDM6fDB?l0{c463w*} zCUddNsn9Iua+gDq1nNf9><Q24F6I{fB}x-^8b{I~&rxc8&T6GPoLR?4H=>hOW+dr5 zqwWhVR)@NU7Gwlfp1xm#WbYU+*C>id-7c$B4sJqDu}eE{(_C(q6zPgvXi9U7U9xuk z<QkV-o48SxiABY(B9~j4;&ZzdXH^4@*5@GUc$`K!s7<a4Hp<qOR-4_r)UIeW+}g)v z=>4Zri&A|%Daq11$TeDzeEqG=r}+9S(|mTjAW2h$JyuX9k*3Nm3zr-AhA&~8j23ok zP$~|vp9VdG1OCRrtzzf_(r{+s6*hfHM&H9Q&$2nI1<0x@@z`D7EVh2gr#%moJ~Yz? znS|H<uAvzq<*_qmBHr|$PcfqRn?&Jl;cazuPg#Uh?8nsP==YJYRFBHe&dklrEA=h4 z+o}ZNeKulPDn8^pN=SBJ8rg%b9+rqt*p6XO;ylY6{xOx`-9JRObo7^m&ynwO|BA}z zY(ZLT@AEY01>u4wP+tpQv&b{CtR+n*P4-S&79E6N(u(m{R-C>@I~2R9>=}_n8t#b^ z1yu9th(F?I|K^dozzzSq88aXP{-{w;f!Yp?ehiQJCuMmN1`<Of{eiufeK*4V{l*Lh z!q~*I8?@(GX2^LsEKKbr2@&9tmFUm*<$Q=p|C-z^AetTgAcK9C$4!xHA5n;A4~@$q zO?YgaOwaS<;_3OXadQywFUv0%{YR&)(H1hPun_+8{4x=#{{NWL9ITrRQmYS4t3f8a zG;J9zn9Z5~2ED}0n2v0=c*aP2K02dT%mojxr-k&H*^2RO(;X#j@EvxoG+*OIU-pkX zmTK?C?4?=Ts5E(YxmW<+dLM1ezt4UFh3u}P$HhXeCXq!JJ4kIe7U$CFBgGG+h!vC! zA!gK;OaZ;@Eqzpbrn2g?HB|bltcLV^@thf`_VYRQAa!h>>!h)bbGM4~z)K!Pe}Bfj zaS|3X`@(s>D~L6sELhc_CP88EFB}USGu<@^k*u{nK1{GtONd!;u(G={$vi)B*UR)A zb@#LMJahL`Six2-nv4ea%Ax{f`F~n8fRuI@1PGO$XPfSdWjTu%#ja4N@IjzIlGLm$ zp+T+45>~UyB{A%c#c@bu=NCuCbTc?f1Z65UqtncKR1QHi%d8xcy%t@VXDxmMdV*D5 zs7I?{i<ZQ)eU*c0lD0|<*0D>K@$@t;i5s^LoghoMbc7k!p}USu`;CrF({$uUTKX_- zX6u*qrzfqAeXwLSwz5!bKDM)|)?vg%mo<)_8?1NZ8TPex{OD&Og->y<QdRFRkS3>x zpgqLh5a^G1(?VFVg-QmKcINjH>tp=RrLT+PL6CvW3F>0=6oMg|Dwgtfn?;aOw~a*Q zb-haV)+!x}nYZ;uOlVNUvR(Jj77u~{xeOWOk3cG?F+!xF3pP<*H#2b*zv01AS|a@& zW!shEd96sBY<E>HFZQX32j;9&4I_C<+_q&lC$5W0owk<7L_u~~8<r}x;0M{il<fYk zfc7~8a`Xn;d7G=+R&|;c)nvw87jY{=L1=}UH6=u|mYNuxV8?5c2pV0i$)R$>viM$I zXwL!cW0CGfZ1=Lhz2bt4kc5N4L8`8hb<B_^cjC8O(cnpTthSOR)y9i$V5nQncGbq? zI#o8|CYxkiD@BSpt<g`uXM_|h;*3g_y>ibGDN)p>Sj6&{CrIfXFNc>8M8y`@5|(2h zfYn>-Dw*G&C^d^XsZq&tB#UcBR$yDqjyQ%%>qMN>>ZdqIOB+R;)m}bwregCJS2J2z zFLxztcO_uomb!bzgZjYF>V`_EMYL&sm##<=&xq`J?P7Lm#b6@P%iT;gH@P2?E_TqX z_YA=|Y=`Gw>1xL^f9D-2h8eVVT=&M%;>^A~rPCllQCeeR$5xHS3EnmsgV`6pqqnW^ zvug8g>-*Q=ciZ}i2G7vbw}@QRZzQfs6Sy|^Lc>hr^VNniwCd)2qgcYTJ^j;vM>;uJ z>2JRGYZ$JOjxKGE@vm9ETy6R8{)|CqVIohf9Rw$2S51vV=*8Vml1%^*X4T$aWRn_W zhMpzwVTNL*MgdtKrOM^3uB_GGyR`^s+3Lp8#L9z>DWoz#Z7d-eY|-5#K{&^<o08b* z=4j?@>K%PMLwb>IX;RsKq^Xef=PylJ;uY|+(PRWln+M`5t80!Qcn!!Xu-jd$l<IO? zoku-ySLt6eSuDae@b=N@%Z@c$Ng@;1j6{aNYz-I1n_%BYS@`s!bR>GOjTa%ZX1|3k zc_f5-6s>iTMbm3mZwf;O`{JPiRR52M(u4J}A=TvJbsxl*J+iH1j7}wCU9S!*5h=?R zrLNLyx2;sjjL%trx1po&pElOh60ZI;+CT5n?2i8WI@n8(9q#C+FXQ1&ht!G*n+vdk zHEm81jUwB*xgXh?!<$D94-+BZ?aLV_kIJY}@h_QkA!LIPMn8rqO-)8nk^Uk@0=@%T z)|OP2THC2~Lk7X6ro;^Yu`QAc3ifTy^j~8Ey*NRyVDZ~bo&6SUyDCc2V)z19W{hD~ z+xz#XrJF3pnv*RFM0LY<OElGWsWf$8V#OfDYukQ+AjO&|XCWe9T^>m-A-6=4xrX** zSDzXXl`JBToR3*b18r!O=6`4T<)_aY$cg*MjuAM;Qg@C=J6pDM5@E7EJB!Gz`)TJ? zdXC$b5_3CQN+(vO^kbWLWl5PLIhaNF_GeR`8>$}<f5Nk&_-%c(zv;PoMr5*&_LRqF zi<nWk!e?9Qs?uTy`7_ewVzPtTY;sGklq-@m+1`@D_P2~6bn!*Y0(xKnQgV+|(tL~; zrE(OoBQFi2g@69ia6&YPUM!*-!%L}zWga`zkDi4u$;mUt;1-U_st|;kDr+WbtSO?R zWEw9Ph(cK>*?FC0(@*IKG^)nt_IMYHu%ygSlgm@dwb0LIw<=*CGwm&jSg28)r&6pC zRlpm|R_{%OmF?J@0-yiwz15(X^q22~!X9~fIX!=Tc@!Puq5G5IV6*qfX4*tkoTsj` ziadslk~%Aa`BFt8UqrQ%-2jf@pqa!KqOgK(+dnj-dy9{~vp*XRTMqQ2<83)0_rCS} zSF`B{(mIqVKk%wv9F8>q{Rc;oaH%J>A3O2yuj<A4VAtpV(+{y(Bp{u@{_@ITGIuW@ zierNg{a!02(eMurjR4zmx+i;bWN$VgkVvp>eBft;xK3p6X7ty!!s6EkXa_TfZF((4 z1L_w4{?~pq=#%tifny1<`9C^F+Q<f^>0yj1sv9RjCV{PUd8~HL0Ce=7-#SN(r@mRG z!Pc3iTk8eHS?j33rWxLR<5Q7%yyLBu*lrjv!>%}&d21_|67_1E|NL7QBypoih<_0K z`rX3d873`NamwBG^2@261h{sd9*iXR=ILvM_+NeR4TNqLHLspM_WlgQ%%8lU(?|0| z)s=Lj4>q9ft&WcVAKG>yVV8(W-GXezRyB$Pi(9dJTu#-ASc%;%<wFhgZ~AbwEklhV zR0d&`2`Rx6VVPeC6_8eEZLqtn)f8S^7F=ApZJE>R^|=+nB-N{Njc#6V&<E~7aVkV3 zG^w1?&(R=Nm|_M)h$^hA_E02hJ1tyMi+OonR7oDUv&&}^?s!J*IBwZ=+@7I&LxFbO zc8T4AUF2_po$4;p-)>d3socVPe>t6W@>3r@9uqZ#;JD2pJM_R-t*uU$H&|KXl&D5e zt;=VxmX&&&$1BSp+Pt#EM<vw(uCdwe8W43Wq+{HQgHWnRPN`DdUQe1_O#{?WXsRq1 zKy~LGR-02^<?_nfT$SDCvU%hx6<1cvR-f19AXTNlHpr`Ll@h1Cl57wKI&IEn9f-F< zmOoc1dK@-ytx7UYR@bFiogVofm(8hmRRJ<l(*t%LA12G}iI1;~`$TqZO1%HTCw)|0 z^v0);;t-qj*^BhN`58y6f9vOuf?kro7%|`o$(|Z<ELGe~>2pbU9#Q#sR3w1x_+sSf z)?2uKf-K6f!Y(JMJ8@n8p(xEsQ8>x|{KZ59m&ARsP5l3m{e)o6XZkZ+d%8r1G@Q3q zpnpW&sJcqRZFRJJUe!e%j(B#Zy`PG4JGbgA8*^a=vi*B5^e{wq)5#Qzs!}84+5We_ zdQKG2iK?q^VGmr&(zRiTSrsQ8*!ur|>AV_k7=NvQP*pmwnaL<OUK_$DT`}sKGKt-D zr6=j!l~>*hy=~ta)cE$bh>&iLvgZ6J_%m)m8rFV`RE+=aSsbsQ#fzdAAK1DQX(-(K z93BwA5m{4lG^ZgJ-9lKn3y8!O-V7uJtwrDs;IW`Fv2eQqJ@sHgV3z@#Zdc!A1nFEg ztdPpT4+VWx;P0WZbs##8L~0SCQo=8!>FXQjHq@yRc#2vn^%SrP_hpVD-C3(ejBWho zo`{DnaH=PgI!WA&#DG9@Z~TWCTz7zKHg+yXYd+Q&JJlL)jYbE4z1`=18;u_@?8+@n zq}BcUXcg&9yehJiIsN!R3t~gBiMWrBD{J*71S%{jGW70TeU}g<1<nn?a<N<I_<%hD zr6R8LqcR2txx6hA?3O-cbYn%mkqmm^=3wL^?3$_$PC&TMCnh5WH+gL`a*Qy5+!PA+ z^TWv)D;f>_YBFYtHwn7J!sidcWDy4b<PbDQ8x5V1#5~`nS{p$K<0FP*Jefy%!9bmz z{rK}kaS=AYJ_UaInUIQ0x3mW+EWtp#a&0*77ZJg)79)o5cmM<VFJ%a&^U|3)i8RoX zk3fhH93O$VfLOI}-q3i19xX`h7A=UUu9B%879mj`M6bzRgr`XQfYfdSQq^vfkZ$O1 zj4}<s*}F{YM<1HWOGhJ)*Jq+1viYV=&}Z|*nMg}0Fko)E=A&8N4fE$rD|IVDCyYjB z>%$U8Ve0UU6k|}mDDvr6y48$V&=BVuNOQYux=g5$%Qud~WDT%U1If9oMi9p9r2a7q zvwC)t5(?B><^O_qAwNGFnYw@1_c#8XFtdwmLyCfYF6+j&BKj!u{>(y|%0GIW7ucQ! zI!X%zPmjUp22}8Z@gzB$^RO_|YCuY1z1MA>X16Z$2!d5>OT48{5)=c>zRXcM0+q<I z3KeYHHb*;x)BCzj@6f3~HIB}83qLXrBYLecAa0I^m1u?)dV54Z28H?56*>$;L)Rxo zuMW~|?nZO9K4x1!<i0KFN{k97m~7Q8{A4~-!<toMs|~{1Zp79FiEZpgY@JGMNY;j5 zli8sA5S_`0j&?!Vs8U{M5H<&%8jo-Tw(>obFsx@6SGMzWlgP_?#z2l`VqiSQImLus zhTEq{w4{ZJn^(b-LF<<(Vdky$c@#=kbqkMmONw-@V|Uk=HMRu5Y+tu8Yq|ZDlr~bc zFyUR!D1^?GatC!fmf-}&^;nm1i{sh}mCOhZ6EN^LY}%)?sl^~1H0W+F-#7)i(xKoR z<u$?rm#3gu#1Wo14S5w8)X(pTv0Q*$5)8M{>~5vb<@QouG=jo<DrTA!tRT>Il%|nP zhqHuMb-oh@f&NrHluUR?E1foE38#76Gz{!D6>(ZfKy@eE4CG$N2Kr6MObYgJ>kMqe zS^hJH!EugP%*4>%Nk|H!eATf%r^$HK+qz<UvrFf-8_1Q84J6(HQbrfGUh$vky}s$T z8{eqQ`$V{6(4vt1SP}a5xoRLkUyYXwR}E@dTDZ!;F2YJ&<CVpjNH$|fG0NzTlxRVC zH#K`*n`P*=;HKZ~HqA|Snrj9D6!M^Kp_=MY-Pv!%Fo~}(#XVw##D6M9F?RC8GO{wy z^TslaiX%TT_^rOtqsW<|u!u%LHVe`Gy)xvGr`oCIV<onEWdA_=98h2)UTw~dlJzg{ zo<>jPPt1jj>_y@{^bIjPDf_FHhl<G(d6sd!{|bZ$-1G1vB~COdDd|DIpDA_CmaZ)% zj3hFXN+P{xOXTe(@h2DH4dn903o*=`abK~^Tj+H8meo#Codmwwfhc^k&5J?2bs?hi zv!(`}2I^Nbi`cWLqe7-T0kV^vhoy?*loj#`d{!?x4CJ!fycD9cH}Lly7;F%wJ$&Cn z(v;y&j3yI1$%#84@!vZ!8%F-+T_`~aA9**XB9uE^m;@7l*@ZC(<KNzmBJ|*Si;#z& z+*yZ5$rMQynBKEM5-c#23t}zc^D8h-DwNQ~o68WzU#Y+yeTpQcs2*v#mpnkymIacU zIOC~{F;F7-u}97Fb7e95jw+P8o5h*ZZ_MIMVjgX)`a`F~EEMq<7o)synab07l0bi| zyc9{oLT;|4+ahxV*_8xbq9@ICYHm`OwF{T(_`s!PTmNZAAO4vYG9R}T35H(M^L(R| zY`$+P649IUrO1bwf4G#yERy%CB3|_2g;f}fzI=5Rl0u`T(m8ZdfM1=ACNe{*)r4cL z5;K3<Kd`BqK!~VF1kq#omKwN4n_3>I%gjg*jzDxs?ENn+T(S}26?~kHh-~Dq+c1sZ zRGA#;3rf?vZqnq_zmpQg4ZQpw4DCh!7HK+<)lp|xJgfO5_t54h@uSp))f$<M<{+81 z-N-a+WR6lIcCE@ldR?df67`J|pICxue)n>u58o`28lUS{lyb$ZT`k!xNpeSpn$aPq z3u+uv*s9Id*Q_6-16|_3J@EJCs05z*e^SuJUETL{_pkS`M~lWsV#2<_4_*+0KgfSv z35pT&S*u9(9pcZdBItWaV@{NyGlz^orz$!U=vj}wQrJ<I`>m4D%8%cNd4#0tCytL= zjX(B0sZDWOvXB)}#Ui>bxSwab?PnX$y`KcLh_ARGSrT~!Nvxo(FMs`h^y+&?rQIe8 zXEj3Sh|oEmP<xQj#cqV!`QS#}-{Yb>rd<-gk;v)l$A?#vU-WJxR*K>kiF}G!zGw}e z#Z~^x8muD)udeGhdw-puegGp0`QLm1$>}hfvTEEEY^wD5>QsX$>nSrm#@K?Oamyj& zW;6yUZ@UL#n2}#!i^7QzwQLzqAgS{pW2+dYfjvYR)gUEFAr%Bkjx<IaV~z2~L}RKk z+n8$<B6;b<=+F5>$RNqq%71wX!|AAN<&=l%u*9q7d?e~sFFcGPJ(7)}dni^xNDh4b zFb<lfRHGgv=g&TY0aMb|Su>5W2R}-?ZIzcdrB<P=po`)=+KEXwnlhGED=RDA`hBUm zVBU>ZD`allj-1{$Kql`}j~}W4UR|qoJ7g)>XwojY@E!ElC*#)_N*?EJ+sX6Y!&_g) zFdq9P>E)Cc_B=`cRTgh~3jab5-}5xVgBgJzo+gJA)<DxAaJLABf4&nQ${z93UHb2v zyGZ#g<b}^7rLO~+Yst{So1Pset<v-?c9U`%@*EBjT>SVsI?&ts*ynMUtV85(T){s6 z%Wjm0Z6L7$Wg6@G{C|U&<o?}%gF+S|^98g-odoVPWffWLZ4RG9C!H4f?gg?tv?&u> z$Qs4+buAb{AmnfhR^wt|%!?=x$-%GRi`Nkw$leFKF_FyMUItzH;=}jTq8{XJ`|%^1 z0&g8aqu8^MuF;uDqtC5Xc>BkssH^;_Kp}7SgE$db{0hzhZGo&mg0g4x`2NGVOXWk4 zBN&D20ZI%14$?xyX1a>-Dyp_UNft5bHN0i0Fzn=4w_|7^>UE4Zkdry%7&f4RpE*Xh zVJ+`{9O?8k{y0*ko4}@k?KqkZhRuc$jyKVF+nY!wwhem|Nj<h}_x$jLw1hwN7A%3f zH}PMFM#`x^E#MhLsD9@T`j1JwRiq(oGWbUGE*dLQG|yV+=>)N|7Kl*esM_tIw66O( z3T4;T0Za7TMysrZ1}OTcMhD#*f)=nx(r@>~R0pHQLX*Q)ZLez>>2-VD<i>1A)IiBD z2U}&r_0{TB`goPDFqzMK8!zGj|LJXF;Q^j{Qu|qSl8)Cw{>Vv;3_G9-1%(Ip^5Z8l zj)daINh~AOT>TEbhNFfG{@FX2gID;7cgb``@TKp<rtZn9cd;I={LWJ#spJ1Vh3{$W zFP|ocpXAB!;f|z4O8f|DGxUm^?xqW$byjo~EKw@pAO@V_>^&NAmY;qP%_Jlxf5v6e zm@eYDiMO6aOkm!B;Q^pMaP|YzZ6tW5AE8Way279P2$TSn__&X;Mye3$5>e~NXcGyH zKK=>H<5!FLRzR2(l{K$!nVa&ade&b+vxpzLbQVKYoSJr)PIsgfEvXDxE8=Hj1NWRI zb05!NJWK03!7rR8yp_mPKSd3F`le6ux@Db+8`4xdCLM`|;8Ee#pJ4&9;OJ+lqGOl% zIc^Z_OgM))<3<ri@CoNoi=%-T&e1&!DO*Yi=Mk4Nlv03_z{g%Dq`Ch~gwW3)zr+wS zQ0KlRZ5qj+J5PGHjL&Yz9BDH}M*`2bQxuGp!Ha*vFhVtZFJps|woppuW$mP9HvKo* znGJmBx0p-Y5`O_DQj3bB>n`9Nti1Rl=~9I+_zs2i_J{8%R%7L#e}_S;iuewW7uNp| z=_eci{a0ic9X#)AjN{*bMKkJUQ>n{k9|b{sdj%%~-+zrhpu;0x!VFa^W?dqKa**G9 z33uWs|KJj-i|t&zj8ST1_GJ_h&{%aDS^uY%xA7g{l7S7b{Ip06)mFZX5BZLw6t`Qs zbVkMZ^8etT7wq5D;&*e$Uy&BxB4rY11)LR;9@za?<cRc%U;Kb%LyL5fhx`r0rE`=< z3}pWeb5$pC{WYwTkE*gzI)}0XlyUY@6rsAIz*|`7s;Z@JGFp!E#GepN>>c|P@%5sJ z0>1tytRO+T@e>6gPV)YLCrx;kfBbiHMZXbAqecCU2c@WQq=|9!yf!=C2oyk98p<He z<_8Cn{yTPq4%HR@w;Lp2SNUBx@erBo_M4a*6=onl#AS3y*<{N3Uk~K|i_R?>Y~>dU znO*1Cf5Ci`twUv^ghr#WAj~MqGdc|1U5eXPuH7hzG;Wxr)t1*ORrATurI2&9(VU~z z&8-jC#2U>RTFrFDsd#Ljc%!WMpc{-{%4B#O5{+_72X#u17&oM8Z~6f+WE(fAhA@^a J`i6Al{{r)XMN|L) delta 10421 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 Wge>3Xt25I7b!m0n1jF(O!+!(Ppp3`> diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index daf069abe..620eb3c8c 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -40,6 +40,7 @@ std = [ 'pallet-certification/std', 'pallet-distance/std', 'pallet-duniter-account/std', + 'pallet-quota/std', 'pallet-duniter-wot/std', 'pallet-grandpa/std', 'pallet-identity/std', @@ -75,6 +76,7 @@ pallet-authority-members = { path = '../../pallets/authority-members', default-f pallet-certification = { path = '../../pallets/certification', default-features = false } pallet-distance = { path = "../../pallets/distance", default-features = false } pallet-duniter-account = { path = '../../pallets/duniter-account', default-features = false } +pallet-quota = { path = '../../pallets/quota', default-features = false } pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false } pallet-identity = { path = '../../pallets/identity', default-features = false } pallet-membership = { path = '../../pallets/membership', default-features = false } diff --git a/runtime/common/src/entities.rs b/runtime/common/src/entities.rs index 08e842079..823ae2a71 100644 --- a/runtime/common/src/entities.rs +++ b/runtime/common/src/entities.rs @@ -75,6 +75,7 @@ macro_rules! declare_session_keys { #[cfg_attr(feature = "std", derive(Deserialize, Serialize))] #[derive(Clone, Encode, Decode, Default, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)] pub struct IdtyData { + /// number of the first claimable UD pub first_eligible_ud: pallet_universal_dividend::FirstEligibleUd, } @@ -93,35 +94,6 @@ impl From<IdtyData> for pallet_universal_dividend::FirstEligibleUd { } } -pub struct NewOwnerKeySigner(sp_core::sr25519::Public); - -impl sp_runtime::traits::IdentifyAccount for NewOwnerKeySigner { - type AccountId = crate::AccountId; - fn into_account(self) -> crate::AccountId { - <[u8; 32]>::from(self.0).into() - } -} - -#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)] -pub struct NewOwnerKeySignature(sp_core::sr25519::Signature); - -impl sp_runtime::traits::Verify for NewOwnerKeySignature { - type Signer = NewOwnerKeySigner; - fn verify<L: sp_runtime::traits::Lazy<[u8]>>(&self, msg: L, signer: &crate::AccountId) -> bool { - use sp_core::crypto::ByteArray as _; - match sp_core::sr25519::Public::from_slice(signer.as_ref()) { - Ok(signer) => self.0.verify(msg, &signer), - Err(()) => false, - } - } -} - -impl From<sp_core::sr25519::Signature> for NewOwnerKeySignature { - fn from(a: sp_core::sr25519::Signature) -> Self { - NewOwnerKeySignature(a) - } -} - #[cfg_attr(feature = "std", derive(Deserialize, Serialize))] #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)] pub struct SmithMembershipMetaData<SessionKeysWrapper> { diff --git a/runtime/common/src/pallets_config.rs b/runtime/common/src/pallets_config.rs index e5aaa8c3f..dc585e3f1 100644 --- a/runtime/common/src/pallets_config.rs +++ b/runtime/common/src/pallets_config.rs @@ -67,7 +67,7 @@ macro_rules! pallets_config { /// What to do if an account is fully reaped from the system. type OnKilledAccount = (); /// The data to be stored in an account. - type AccountData = pallet_duniter_account::AccountData<Balance>; + type AccountData = pallet_duniter_account::AccountData<Balance, IdtyIndex>; /// Weight information for the extrinsics of this pallet. type SystemWeightInfo = common_runtime::weights::frame_system::WeightInfo<Runtime>; /// This is used as an identifier of the chain. 42 is the generic substrate prefix. @@ -101,11 +101,36 @@ macro_rules! pallets_config { // ACCOUNT // impl pallet_duniter_account::Config for Runtime { + type RuntimeEvent = RuntimeEvent; type AccountIdToSalt = sp_runtime::traits::ConvertInto; type MaxNewAccountsPerBlock = frame_support::pallet_prelude::ConstU32<1>; type NewAccountPrice = frame_support::traits::ConstU64<300>; - type RuntimeEvent = RuntimeEvent; type WeightInfo = common_runtime::weights::pallet_duniter_account::WeightInfo<Runtime>; + // does currency adapter in any case, but adds "refund with quota" feature + type InnerOnChargeTransaction = CurrencyAdapter<Balances, HandleFees>; + type Refund = Quota; + } + + // QUOTA // + pub struct TreasuryAccountId; + impl frame_support::pallet_prelude::Get<AccountId> for TreasuryAccountId { + fn get() -> AccountId { + // TODO optimize: make this a constant + // calling Treasury.account_id() actually requires computation + Treasury::account_id() + } + } + parameter_types! { + pub const ReloadRate: BlockNumber = 1 * HOURS; // faster than DAYS + pub const MaxQuota: Balance = 1000; // 10 ÄžD + } + impl pallet_quota::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + // type IdtyId = IdtyIndex; + type ReloadRate = ReloadRate; + type MaxQuota = MaxQuota; + type RefundAccount = TreasuryAccountId; + type WeightInfo = common_runtime::weights::pallet_quota::WeightInfo<Runtime>; } // BLOCK CREATION // @@ -113,22 +138,16 @@ macro_rules! pallets_config { impl pallet_babe::Config for Runtime { type EpochDuration = EpochDuration; type ExpectedBlockTime = ExpectedBlockTime; - // session module is the trigger type EpochChangeTrigger = pallet_babe::ExternalTrigger; - type DisabledValidators = Session; - type KeyOwnerProof = <Historical as KeyOwnerProofSystem<( KeyTypeId, pallet_babe::AuthorityId, )>>::Proof; - type EquivocationReportSystem = pallet_babe::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>; - type WeightInfo = common_runtime::weights::pallet_babe::WeightInfo<Runtime>; - type MaxAuthorities = MaxAuthorities; } @@ -142,6 +161,7 @@ macro_rules! pallets_config { // MONEY MANAGEMENT // impl pallet_balances::Config for Runtime { + type RuntimeEvent = RuntimeEvent; type MaxLocks = MaxLocks; type MaxReserves = frame_support::pallet_prelude::ConstU32<5>; type ReserveIdentifier = [u8; 8]; @@ -154,8 +174,6 @@ macro_rules! pallets_config { type FreezeIdentifier = (); type MaxHolds = frame_support::pallet_prelude::ConstU32<0>; type MaxFreezes = frame_support::pallet_prelude::ConstU32<0>; - /// The ubiquitous event type. - type RuntimeEvent = RuntimeEvent; type WeightInfo = common_runtime::weights::pallet_balances::WeightInfo<Runtime>; } @@ -171,19 +189,29 @@ macro_rules! pallets_config { } } + // fees are moved to the treasury pub struct HandleFees; type NegativeImbalance = <Balances as frame_support::traits::Currency<AccountId>>::NegativeImbalance; impl frame_support::traits::OnUnbalanced<NegativeImbalance> for HandleFees { fn on_nonzero_unbalanced(amount: NegativeImbalance) { use frame_support::traits::Currency as _; - if let Some(author) = Authorship::author() { - Balances::resolve_creating(&author, amount); - } + // fee is moved to treasury + Balances::resolve_creating(&Treasury::account_id(), amount); + // should move the tip to author + // if let Some(author) = Authorship::author() { + // Balances::resolve_creating(&author, amount); + // } } } pub struct OnChargeTransaction; + + parameter_types! { + pub FeeMultiplier: pallet_transaction_payment::Multiplier = pallet_transaction_payment::Multiplier::one(); + } impl pallet_transaction_payment::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + // does a filter on the call type OnChargeTransaction = OneshotAccount; type OperationalFeeMultiplier = frame_support::traits::ConstU8<5>; #[cfg(not(feature = "runtime-benchmarks"))] @@ -191,13 +219,13 @@ macro_rules! pallets_config { #[cfg(feature = "runtime-benchmarks")] type WeightToFee = frame_support::weights::ConstantMultiplier::<u64, sp_core::ConstU64<0u64>>; type LengthToFee = common_runtime::fees::LengthToFeeImpl<Balance>; - type FeeMultiplierUpdate = (); - type RuntimeEvent = RuntimeEvent; + type FeeMultiplierUpdate = pallet_transaction_payment::ConstFeeMultiplier<FeeMultiplier>; } impl pallet_oneshot_account::Config for Runtime { - type Currency = Balances; type RuntimeEvent = RuntimeEvent; - type InnerOnChargeTransaction = CurrencyAdapter<Balances, HandleFees>; + type Currency = Balances; + // when call is not oneshot account, fall back to duniter-account implementation + type InnerOnChargeTransaction = Account; type WeightInfo = common_runtime::weights::pallet_oneshot_account::WeightInfo<Runtime>; } @@ -207,6 +235,7 @@ macro_rules! pallets_config { type MaxAuthorities = MaxAuthorities; } impl pallet_authority_members::Config for Runtime { + type RuntimeEvent = RuntimeEvent; type KeysWrapper = opaque::SessionKeysWrapper; type IsMember = SmithMembership; type OnNewSession = OnNewSessionHandler<Runtime>; @@ -215,7 +244,6 @@ macro_rules! pallets_config { type MemberIdOf = common_runtime::providers::IdentityIndexOf<Self>; type MaxAuthorities = MaxAuthorities; type RemoveMemberOrigin = EnsureRoot<Self::AccountId>; - type RuntimeEvent = RuntimeEvent; type WeightInfo = common_runtime::weights::pallet_authority_members::WeightInfo<Runtime>; } impl pallet_authorship::Config for Runtime { @@ -223,8 +251,8 @@ macro_rules! pallets_config { type EventHandler = ImOnline; } impl pallet_im_online::Config for Runtime { - type AuthorityId = ImOnlineId; type RuntimeEvent = RuntimeEvent; + type AuthorityId = ImOnlineId; type ValidatorSet = Historical; type NextSessionRotation = Babe; type ReportUnresponsiveness = Offences; @@ -259,21 +287,18 @@ macro_rules! pallets_config { } impl pallet_grandpa::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type KeyOwnerProof = <Historical as KeyOwnerProofSystem<(KeyTypeId, GrandpaId)>>::Proof; - type EquivocationReportSystem = pallet_grandpa::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>; - type WeightInfo = common_runtime::weights::pallet_grandpa::WeightInfo<Runtime>; - type MaxAuthorities = MaxAuthorities; type MaxSetIdSessionEntries = MaxSetIdSessionEntries; } -parameter_types! { - pub const MaxSetIdSessionEntries: u32 = 1000;//BondingDuration::get() * SessionsPerEra::get(); -} + parameter_types! { + // BondingDuration::get() * SessionsPerEra::get(); + pub const MaxSetIdSessionEntries: u32 = 1000; + } // ONCHAIN GOVERNANCE // @@ -301,8 +326,8 @@ parameter_types! { } impl pallet_preimage::Config for Runtime { - type WeightInfo = common_runtime::weights::pallet_preimage::WeightInfo<Runtime>; type RuntimeEvent = RuntimeEvent; + type WeightInfo = common_runtime::weights::pallet_preimage::WeightInfo<Runtime>; type Currency = Balances; type ManagerOrigin = EnsureRoot<AccountId>; type BaseDeposit = PreimageBaseDeposit; @@ -318,6 +343,7 @@ parameter_types! { } impl pallet_provide_randomness::Config for Runtime { + type RuntimeEvent = RuntimeEvent; type Currency = Balances; type GetCurrentEpochIndex = GetCurrentEpochIndex<Self>; type MaxRequests = frame_support::traits::ConstU32<100>; @@ -326,7 +352,6 @@ parameter_types! { type OnUnbalanced = Treasury; type ParentBlockRandomness = pallet_babe::ParentBlockRandomness<Self>; type RandomnessFromOneEpochAgo = pallet_babe::RandomnessFromOneEpochAgo<Self>; - type RuntimeEvent = RuntimeEvent; type WeightInfo = common_runtime::weights::pallet_provide_randomness::WeightInfo<Runtime>; } @@ -413,7 +438,7 @@ parameter_types! { impl pallet_universal_dividend::Config for Runtime { type MomentIntoBalance = sp_runtime::traits::ConvertInto; - type Currency = pallet_balances::Pallet<Runtime>; + type Currency = Balances; type RuntimeEvent = RuntimeEvent; type MaxPastReeval = frame_support::traits::ConstU32<160>; type MembersCount = MembersCount; @@ -444,14 +469,13 @@ parameter_types! { type IdtyCreationPeriod = IdtyCreationPeriod; type IdtyData = IdtyData; type IdtyIndex = IdtyIndex; + type AccountLinker = Account; type IdtyNameValidator = IdtyNameValidatorImpl; type IdtyRemovalOtherReason = pallet_duniter_wot::IdtyRemovalWotReason; - type NewOwnerKeySigner = <NewOwnerKeySignature as sp_runtime::traits::Verify>::Signer; - type NewOwnerKeySignature = NewOwnerKeySignature; - type OnIdtyChange = (common_runtime::handlers::OnIdtyChangeHandler<Runtime>, Wot); + type Signer = <Signature as sp_runtime::traits::Verify>::Signer; + type Signature = Signature; + type OnIdtyChange = (common_runtime::handlers::OnIdtyChangeHandler<Runtime>, Wot, Quota, Account); type RemoveIdentityConsumers = RemoveIdentityConsumersImpl<Self>; - type RevocationSigner = <Signature as sp_runtime::traits::Verify>::Signer; - type RevocationSignature = Signature; type RuntimeEvent = RuntimeEvent; type WeightInfo = common_runtime::weights::pallet_identity::WeightInfo<Runtime>; #[cfg(feature = "runtime-benchmarks")] @@ -549,7 +573,7 @@ parameter_types! { } parameter_types! { pub const TechnicalCommitteeMotionDuration: BlockNumber = 7 * DAYS; -pub MaxProposalWeight: Weight = Perbill::from_percent(50) * BlockWeights::get().max_block; + pub MaxProposalWeight: Weight = Perbill::from_percent(50) * BlockWeights::get().max_block; } impl pallet_collective::Config<Instance2> for Runtime { type RuntimeOrigin = RuntimeOrigin; @@ -559,8 +583,8 @@ pub MaxProposalWeight: Weight = Perbill::from_percent(50) * BlockWeights::get(). type MaxProposals = frame_support::pallet_prelude::ConstU32<20>; type MaxMembers = frame_support::pallet_prelude::ConstU32<100>; type WeightInfo = common_runtime::weights::pallet_collective::WeightInfo<Runtime>; -type SetMembersOrigin = EnsureRoot<AccountId>; -type MaxProposalWeight = MaxProposalWeight; + type SetMembersOrigin = EnsureRoot<AccountId>; + type MaxProposalWeight = MaxProposalWeight; #[cfg(not(feature = "runtime-benchmarks"))] type DefaultVote = TechnicalCommitteeDefaultVote; #[cfg(feature = "runtime-benchmarks")] diff --git a/runtime/common/src/weights.rs b/runtime/common/src/weights.rs index 952423d90..f8ab1253a 100644 --- a/runtime/common/src/weights.rs +++ b/runtime/common/src/weights.rs @@ -40,6 +40,7 @@ pub mod pallet_identity; pub mod pallet_preimage; pub mod pallet_utility; pub mod pallet_duniter_account; +pub mod pallet_quota; pub mod pallet_oneshot_account; pub mod pallet_certification_cert; pub mod pallet_certification_smith_cert; diff --git a/runtime/common/src/weights/pallet_duniter_account.rs b/runtime/common/src/weights/pallet_duniter_account.rs index 62e4d4e49..675cf9303 100644 --- a/runtime/common/src/weights/pallet_duniter_account.rs +++ b/runtime/common/src/weights/pallet_duniter_account.rs @@ -48,6 +48,17 @@ use core::marker::PhantomData; /// Weight functions for `pallet_duniter_account`. pub struct WeightInfo<T>(PhantomData<T>); impl<T: frame_system::Config> pallet_duniter_account::WeightInfo for WeightInfo<T> { + /// Storage: System Account (r:1 w:0) + /// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen) + fn unlink_identity() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3591` + // Minimum execution time: 95_130_000 picoseconds. + Weight::from_parts(110_501_000, 0) + .saturating_add(Weight::from_parts(0, 3591)) + .saturating_add(T::DbWeight::get().reads(1)) + } /// Storage: Account PendingNewAccounts (r:1 w:1) /// Proof Skipped: Account PendingNewAccounts (max_values: None, max_size: None, mode: Measured) /// Storage: ProvideRandomness RequestIdProvider (r:1 w:1) diff --git a/runtime/common/src/weights/pallet_identity.rs b/runtime/common/src/weights/pallet_identity.rs index 2d2e0eb35..11cea0a9a 100644 --- a/runtime/common/src/weights/pallet_identity.rs +++ b/runtime/common/src/weights/pallet_identity.rs @@ -236,4 +236,20 @@ impl<T: frame_system::Config> pallet_identity::WeightInfo for WeightInfo<T> { .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: Identity IdentityIndexOf (r:1 w:0) + /// Proof Skipped: Identity IdentityIndexOf (max_values: None, max_size: None, mode: Measured) + /// Storage: System BlockHash (r:1 w:0) + /// Proof: System BlockHash (max_values: None, max_size: Some(44), added: 2519, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen) + fn link_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `359` + // Estimated: `3824` + // Minimum execution time: 543_046_000 picoseconds. + Weight::from_parts(544_513_000, 0) + .saturating_add(Weight::from_parts(0, 3824)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(1)) + } } diff --git a/runtime/common/src/weights/pallet_quota.rs b/runtime/common/src/weights/pallet_quota.rs new file mode 100644 index 000000000..eed488194 --- /dev/null +++ b/runtime/common/src/weights/pallet_quota.rs @@ -0,0 +1,95 @@ +// Copyright 2021-2022 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +//! Autogenerated weights for `pallet_quota` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-10-26, STEPS: `5`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Albatros`, CPU: `Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("gdev-benchmark"), DB CACHE: 1024 + +// Executed Command: +// ./target/debug/duniter +// benchmark +// pallet +// --chain=gdev-benchmark +// --steps=5 +// --repeat=2 +// --pallet=pallet_quota +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./ +// --header=./file_header.txt + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_quota`. +pub struct WeightInfo<T>(PhantomData<T>); +impl<T: frame_system::Config> pallet_quota::WeightInfo for WeightInfo<T> { + /// Storage: Quota RefundQueue (r:1 w:1) + /// Proof: Quota RefundQueue (max_values: Some(1), max_size: Some(11266), added: 11761, mode: MaxEncodedLen) + fn queue_refund() -> Weight { + // Proof Size summary in bytes: + // Measured: `42` + // Estimated: `12751` + // Minimum execution time: 73_265_000 picoseconds. + Weight::from_parts(77_698_000, 0) + .saturating_add(Weight::from_parts(0, 12751)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: Quota IdtyQuota (r:1 w:1) + /// Proof: Quota IdtyQuota (max_values: None, max_size: Some(24), added: 2499, mode: MaxEncodedLen) + fn spend_quota() -> Weight { + // Proof Size summary in bytes: + // Measured: `137` + // Estimated: `3489` + // Minimum execution time: 147_746_000 picoseconds. + Weight::from_parts(165_850_000, 0) + .saturating_add(Weight::from_parts(0, 3489)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: Quota IdtyQuota (r:1 w:1) + /// Proof: Quota IdtyQuota (max_values: None, max_size: Some(24), added: 2499, mode: MaxEncodedLen) + fn try_refund() -> Weight { + // Proof Size summary in bytes: + // Measured: `137` + // Estimated: `3489` + // Minimum execution time: 367_239_000 picoseconds. + Weight::from_parts(392_186_000, 0) + .saturating_add(Weight::from_parts(0, 3489)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + fn do_refund() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 356_707_000 picoseconds. + Weight::from_parts(471_930_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + } +} diff --git a/runtime/g1/Cargo.toml b/runtime/g1/Cargo.toml index 13433cb58..b25a268da 100644 --- a/runtime/g1/Cargo.toml +++ b/runtime/g1/Cargo.toml @@ -46,6 +46,7 @@ std = [ 'pallet-distance/std', 'pallet-duniter-test-parameters/std', 'pallet-duniter-account/std', + 'pallet-quota/std', 'pallet-duniter-wot/std', 'pallet-grandpa/std', 'pallet-identity/std', @@ -115,6 +116,7 @@ pallet-certification = { path = '../../pallets/certification', default-features pallet-distance = { path = "../../pallets/distance", default-features = false } pallet-duniter-test-parameters = { path = '../../pallets/duniter-test-parameters', default-features = false } pallet-duniter-account = { path = '../../pallets/duniter-account', default-features = false } +pallet-quota = { path = '../../pallets/quota', default-features = false } pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false } pallet-identity = { path = '../../pallets/identity', default-features = false } pallet-membership = { path = '../../pallets/membership', default-features = false } diff --git a/runtime/g1/src/lib.rs b/runtime/g1/src/lib.rs index 57681a894..920d06390 100644 --- a/runtime/g1/src/lib.rs +++ b/runtime/g1/src/lib.rs @@ -48,7 +48,9 @@ use pallet_grandpa::fg_primitives; use pallet_grandpa::{AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList}; use sp_api::impl_runtime_apis; use sp_core::OpaqueMetadata; -use sp_runtime::traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, NumberFor, OpaqueKeys}; +use sp_runtime::traits::{ + AccountIdLookup, BlakeTwo256, Block as BlockT, NumberFor, One, OpaqueKeys, +}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, transaction_validity::{TransactionSource, TransactionValidity}, @@ -253,6 +255,7 @@ construct_runtime!( Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>} = 6, TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event<T>} = 32, OneshotAccount: pallet_oneshot_account::{Pallet, Call, Storage, Event<T>} = 7, + Quota: pallet_quota::{Pallet, Storage, Config<T>, Event<T>} = 66, // Consensus support. AuthorityMembers: pallet_authority_members::{Pallet, Call, Storage, Config<T>, Event<T>} = 10, diff --git a/runtime/gdev/Cargo.toml b/runtime/gdev/Cargo.toml index c7a84e499..93f07bc78 100644 --- a/runtime/gdev/Cargo.toml +++ b/runtime/gdev/Cargo.toml @@ -29,6 +29,7 @@ runtime-benchmarks = [ 'pallet-collective/runtime-benchmarks', 'pallet-duniter-test-parameters/runtime-benchmarks', 'pallet-duniter-account/runtime-benchmarks', + 'pallet-quota/runtime-benchmarks', 'pallet-duniter-wot/runtime-benchmarks', 'pallet-grandpa/runtime-benchmarks', 'pallet-identity/runtime-benchmarks', @@ -67,6 +68,7 @@ std = [ 'pallet-distance/std', 'pallet-duniter-test-parameters/std', 'pallet-duniter-account/std', + 'pallet-quota/std', 'pallet-duniter-wot/std', 'pallet-grandpa/std', 'pallet-identity/std', @@ -141,6 +143,7 @@ pallet-certification = { path = '../../pallets/certification', default-features pallet-distance = { path = "../../pallets/distance", default-features = false } pallet-duniter-test-parameters = { path = '../../pallets/duniter-test-parameters', default-features = false } pallet-duniter-account = { path = '../../pallets/duniter-account', default-features = false } +pallet-quota = { path = '../../pallets/quota', default-features = false } pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false } pallet-identity = { path = '../../pallets/identity', default-features = false } pallet-membership = { path = '../../pallets/membership', default-features = false } diff --git a/runtime/gdev/src/lib.rs b/runtime/gdev/src/lib.rs index fd22c7bf6..15abf4260 100644 --- a/runtime/gdev/src/lib.rs +++ b/runtime/gdev/src/lib.rs @@ -55,7 +55,9 @@ use pallet_grandpa::fg_primitives; use pallet_grandpa::{AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList}; use sp_api::impl_runtime_apis; use sp_core::OpaqueMetadata; -use sp_runtime::traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, NumberFor, OpaqueKeys}; +use sp_runtime::traits::{ + AccountIdLookup, BlakeTwo256, Block as BlockT, NumberFor, One, OpaqueKeys, +}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, transaction_validity::{TransactionSource, TransactionValidity}, @@ -148,6 +150,7 @@ mod benches { [pallet_provide_randomness, ProvideRandomness] [pallet_upgrade_origin, UpgradeOrigin] [pallet_duniter_account, Account] + [pallet_quota, Quota] [pallet_identity, Identity] [pallet_membership, Membership] [pallet_membership, SmithMembership] @@ -296,7 +299,7 @@ construct_runtime!( { // Basic stuff System: frame_system::{Pallet, Call, Config, Storage, Event<T>} = 0, - Account: pallet_duniter_account::{Pallet, Storage, Config<T>, Event<T>} = 1, + Account: pallet_duniter_account::{Pallet, Call, Storage, Config<T>, Event<T>} = 1, Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event<T>} = 2, // Block creation @@ -310,6 +313,7 @@ construct_runtime!( Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>} = 6, TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event<T>} = 32, OneshotAccount: pallet_oneshot_account::{Pallet, Call, Storage, Event<T>} = 7, + Quota: pallet_quota::{Pallet, Storage, Config<T>, Event<T>} = 66, // Consensus support AuthorityMembers: pallet_authority_members::{Pallet, Call, Storage, Config<T>, Event<T>} = 10, diff --git a/runtime/gdev/tests/common/mod.rs b/runtime/gdev/tests/common/mod.rs index 8561f1655..66b0a2da0 100644 --- a/runtime/gdev/tests/common/mod.rs +++ b/runtime/gdev/tests/common/mod.rs @@ -50,7 +50,7 @@ pub const NAMES: [&str; 6] = ["Alice", "Bob", "Charlie", "Dave", "Eve", "Ferdie" pub struct ExtBuilder { // endowed accounts with balances - initial_accounts: BTreeMap<AccountId, GenesisAccountData<Balance>>, + initial_accounts: BTreeMap<AccountId, GenesisAccountData<Balance, u32>>, initial_authorities_len: usize, initial_identities: BTreeMap<IdtyName, AccountId>, initial_smiths: Vec<AuthorityKeys>, @@ -73,8 +73,8 @@ impl ExtBuilder { get_account_id_from_seed::<sr25519::Public>(NAMES[i]), GenesisAccountData { balance: 0, - is_identity: true, random_id: H256([i as u8; 32]), + idty_id: Some(i as u32 + 1), }, ) }) @@ -231,6 +231,16 @@ impl ExtBuilder { .assimilate_storage(&mut t) .unwrap(); + pallet_quota::GenesisConfig::<Runtime> { + identities: initial_identities + .iter() + .enumerate() + .map(|(i, _)| i as u32 + 1) + .collect(), + } + .assimilate_storage(&mut t) + .unwrap(); + pallet_membership::GenesisConfig::<Runtime, Instance1> { memberships: (1..=initial_identities.len()) .map(|i| { diff --git a/runtime/gdev/tests/integration_tests.rs b/runtime/gdev/tests/integration_tests.rs index 012e6a3b6..1690e0a49 100644 --- a/runtime/gdev/tests/integration_tests.rs +++ b/runtime/gdev/tests/integration_tests.rs @@ -18,11 +18,13 @@ mod common; use common::*; use frame_support::instances::Instance1; +use frame_support::traits::StoredMap; use frame_support::traits::{Get, PalletInfo, StorageInfo, StorageInfoTrait}; use frame_support::{assert_noop, assert_ok}; use frame_support::{StorageHasher, Twox128}; use gdev_runtime::*; use pallet_duniter_wot::IdtyRemovalWotReason; +use sp_core::Encode; use sp_keyring::AccountKeyring; use sp_runtime::MultiAddress; @@ -704,6 +706,9 @@ fn test_create_new_account_with_insufficient_balance() { .build() .execute_with(|| { run_to_block(2); + // Treasury should start empty + // FIXME it actually starts with ED + assert_eq!(Balances::free_balance(Treasury::account_id()), 100); // Should be able to transfer 4 units to a new account assert_ok!(Balances::transfer( @@ -747,7 +752,7 @@ fn test_create_new_account_with_insufficient_balance() { Balances::free_balance(AccountKeyring::Eve.to_account_id()), 0 ); - // 100 initial + 300 recycled from Eve account's destructuion + // 100 initial + 300 recycled from Eve account's destruction assert_eq!(Balances::free_balance(Treasury::account_id()), 400); }); } @@ -760,6 +765,7 @@ fn test_create_new_account() { .build() .execute_with(|| { run_to_block(2); + assert_eq!(Balances::free_balance(Treasury::account_id()), 100); // Should be able to transfer 5 units to a new account assert_ok!(Balances::transfer( @@ -1118,3 +1124,135 @@ fn test_oneshot_accounts() { ); }); } + +/// test currency transfer +/// (does not take fees into account because it's only calls, not extrinsics) +#[test] +fn test_transfer() { + ExtBuilder::new(1, 3, 4) + .with_initial_balances(vec![ + (AccountKeyring::Alice.to_account_id(), 10_000), + (AccountKeyring::Eve.to_account_id(), 10_000), + ]) + .build() + .execute_with(|| { + // Alice gives 500 to Eve + assert_ok!(Balances::transfer_allow_death( + frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), + AccountKeyring::Eve.to_account_id().into(), + 500 + )); + // check amounts + assert_eq!( + Balances::free_balance(AccountKeyring::Alice.to_account_id()), + 10_000 - 500 + ); + assert_eq!( + Balances::free_balance(AccountKeyring::Eve.to_account_id()), + 10_000 + 500 + ); + }) +} + +/// test linking account to identity +#[test] +fn test_link_account() { + ExtBuilder::new(1, 3, 4).build().execute_with(|| { + let genesis_hash = System::block_hash(0); + let alice = AccountKeyring::Alice.to_account_id(); + let ferdie = AccountKeyring::Ferdie.to_account_id(); + let payload = (b"link", genesis_hash, 1u32, ferdie.clone()).encode(); + let signature = AccountKeyring::Ferdie.sign(&payload); + // Ferdie's account can be linked to Alice identity + assert_ok!(Identity::link_account( + frame_system::RawOrigin::Signed(alice).into(), + ferdie, + signature.into() + )); + }) +} + +/// test change owner key +#[test] +fn test_change_owner_key() { + ExtBuilder::new(1, 3, 4).build().execute_with(|| { + let genesis_hash = System::block_hash(0); + let dave = AccountKeyring::Dave.to_account_id(); + let ferdie = AccountKeyring::Ferdie.to_account_id(); + let payload = (b"icok", genesis_hash, 4u32, dave.clone()).encode(); + let signature = AccountKeyring::Ferdie.sign(&payload); + // Dave can change his owner key to Ferdie's + assert_ok!(Identity::change_owner_key( + frame_system::RawOrigin::Signed(dave).into(), + ferdie, + signature.into() + )); + }) +} + +/// test genesis account of identity is linked to identity +// (and account without identity is not linked) +#[test] +fn test_genesis_account_of_identity_linked() { + ExtBuilder::new(1, 3, 4) + .with_initial_balances(vec![(AccountKeyring::Eve.to_account_id(), 8888)]) + .build() + .execute_with(|| { + // Alice account + let account_id = AccountKeyring::Alice.to_account_id(); + // Alice identity index is 1 + assert_eq!(Identity::identity_index_of(&account_id), Some(1)); + // get account data + let account_data = frame_system::Pallet::<Runtime>::get(&account_id); + assert_eq!(account_data.linked_idty, Some(1)); + // Eve is not member, her account has no linked identity + assert_eq!( + frame_system::Pallet::<Runtime>::get(&AccountKeyring::Eve.to_account_id()) + .linked_idty, + None + ); + }) +} + +/// test unlink identity from account +#[test] +fn test_unlink_identity() { + ExtBuilder::new(1, 3, 4).build().execute_with(|| { + let alice_account = AccountKeyring::Alice.to_account_id(); + // check that Alice is 1 + assert_eq!(Identity::identity_index_of(&alice_account), Some(1)); + + // Alice can unlink her identity from her account + assert_ok!(Account::unlink_identity( + frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), + )); + + // Alice account has been unlinked + assert_eq!( + frame_system::Pallet::<Runtime>::get(&alice_account).linked_idty, + None + ); + }) +} + +/// test that the account of a newly created identity is linked to the identity +#[test] +fn test_new_account_linked() { + ExtBuilder::new(1, 3, 4).build().execute_with(|| { + let eve_account = AccountKeyring::Eve.to_account_id(); + assert_eq!( + frame_system::Pallet::<Runtime>::get(&eve_account).linked_idty, + None + ); + // Alice creates identity for Eve + assert_ok!(Identity::create_identity( + frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(), + eve_account.clone(), + )); + // then eve account should be linked to her identity + assert_eq!( + frame_system::Pallet::<Runtime>::get(&eve_account).linked_idty, + Some(5) + ); + }) +} diff --git a/runtime/gdev/tests/xt_tests.rs b/runtime/gdev/tests/xt_tests.rs new file mode 100644 index 000000000..1663d6221 --- /dev/null +++ b/runtime/gdev/tests/xt_tests.rs @@ -0,0 +1,192 @@ +// Copyright 2021 Axiom-Team +// +// This file is part of Duniter-v2S. +// +// Duniter-v2S is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, version 3 of the License. +// +// Duniter-v2S 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. + +// these integration tests aim to test fees and extrinsic-related externalities + +mod common; + +use common::*; +use frame_support::assert_ok; +use frame_support::inherent::Extrinsic; +use frame_support::traits::OnIdle; +use gdev_runtime::*; +use sp_core::Encode; +use sp_core::Pair; +use sp_keyring::AccountKeyring; +use sp_runtime::generic::SignedPayload; + +/// get extrinsic for given call +fn get_unchecked_extrinsic( + call: RuntimeCall, + era: u64, + block: u64, + signer: AccountKeyring, + tip: Balance, +) -> UncheckedExtrinsic { + let extra: gdev_runtime::SignedExtra = ( + frame_system::CheckNonZeroSender::<gdev_runtime::Runtime>::new(), + frame_system::CheckSpecVersion::<gdev_runtime::Runtime>::new(), + frame_system::CheckTxVersion::<gdev_runtime::Runtime>::new(), + frame_system::CheckGenesis::<gdev_runtime::Runtime>::new(), + frame_system::CheckMortality::<gdev_runtime::Runtime>::from( + sp_runtime::generic::Era::mortal(era, block), + ), + frame_system::CheckNonce::<gdev_runtime::Runtime>::from(0u32).into(), + frame_system::CheckWeight::<gdev_runtime::Runtime>::new(), + pallet_transaction_payment::ChargeTransactionPayment::<gdev_runtime::Runtime>::from(tip), + ); + let payload = SignedPayload::new(call.clone(), extra.clone()).unwrap(); + let origin = signer; + let sig = payload.using_encoded(|payload| origin.pair().sign(payload)); + + UncheckedExtrinsic::new( + call, + Some((origin.to_account_id().into(), sig.into(), extra)), + ) + .unwrap() +} + +/// test currency transfer with extrinsic +// the signer account should pay fees and a tip +// the treasury should get the fees +#[test] +fn test_transfer_xt() { + ExtBuilder::new(1, 3, 4) + .with_initial_balances(vec![ + (AccountKeyring::Alice.to_account_id(), 10_000), + (AccountKeyring::Eve.to_account_id(), 10_000), + ]) + .build() + .execute_with(|| { + let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: AccountKeyring::Eve.to_account_id().into(), + value: 500, + }); + + // 1 cÄžD of tip + let xt = get_unchecked_extrinsic(call, 4u64, 8u64, AccountKeyring::Alice, 1u64); + // let info = xt.get_dispatch_info(); + // println!("dispatch info:\n\t {:?}\n", info); + + assert_eq!(Balances::free_balance(Treasury::account_id()), 100); + // Alice gives 500 to Eve + assert_ok!(Executive::apply_extrinsic(xt)); + // check amounts + assert_eq!( + Balances::free_balance(AccountKeyring::Alice.to_account_id()), + 10_000 - 500 - 3 // initial - transfered - fees + ); + assert_eq!( + Balances::free_balance(AccountKeyring::Eve.to_account_id()), + 10_000 + 500 // initial + transfered + ); + assert_eq!(Balances::free_balance(Treasury::account_id()), 100 + 3); + }) +} + +/// test that fees are added to the refund queue +#[test] +fn test_refund_queue() { + ExtBuilder::new(1, 3, 4) + .with_initial_balances(vec![ + (AccountKeyring::Alice.to_account_id(), 10_000), + (AccountKeyring::Eve.to_account_id(), 10_000), + ]) + .build() + .execute_with(|| { + let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: AccountKeyring::Eve.to_account_id().into(), + value: 500, + }); + + // 1 cÄžD of tip + let xt = get_unchecked_extrinsic(call, 4u64, 8u64, AccountKeyring::Alice, 1u64); + assert_ok!(Executive::apply_extrinsic(xt)); + + // check that refund was added to the queue + assert_eq!( + pallet_quota::RefundQueue::<Runtime>::get() + .first() + .expect("a refund should have been added to the queue"), + &pallet_quota::pallet::Refund { + account: AccountKeyring::Alice.to_account_id(), + identity: 1u32, + amount: 2u64 + } + ); + }) +} + +/// test refund on_idle +#[test] +fn test_refund_on_idle() { + ExtBuilder::new(1, 3, 4) + .with_initial_balances(vec![ + (AccountKeyring::Alice.to_account_id(), 10_000), + (AccountKeyring::Eve.to_account_id(), 10_000), + ]) + .build() + .execute_with(|| { + let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: AccountKeyring::Eve.to_account_id().into(), + value: 500, + }); + + // 1 cÄžD of tip + let xt = get_unchecked_extrinsic(call, 4u64, 8u64, AccountKeyring::Alice, 1u64); + assert_ok!(Executive::apply_extrinsic(xt)); + + // call on_idle to activate refund + Quota::on_idle(System::block_number(), Weight::from(1_000_000_000)); + + // check that refund event existed + System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::Refunded { + who: AccountKeyring::Alice.to_account_id(), + identity: 1u32, + amount: 1u64, + })); + + // check that refund queue is empty + assert!(pallet_quota::RefundQueue::<Runtime>::get().is_empty()); + assert_eq!( + Balances::free_balance(AccountKeyring::Alice.to_account_id()), + 10_000 - 500 - 1 - 2 + 1 // initial - transfered - tip - fees + refunded fees + ); + }) +} + +/// test no refund when no identity linked +#[test] +fn test_no_refund() { + ExtBuilder::new(1, 3, 4) + .with_initial_balances(vec![ + (AccountKeyring::Alice.to_account_id(), 10_000), + (AccountKeyring::Eve.to_account_id(), 10_000), + ]) + .build() + .execute_with(|| { + // Eve → Alice + let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death { + dest: AccountKeyring::Alice.to_account_id().into(), + value: 500, + }); + let xt = get_unchecked_extrinsic(call, 4u64, 8u64, AccountKeyring::Eve, 1u64); + assert_ok!(Executive::apply_extrinsic(xt)); + // check that refund queue is empty + assert!(pallet_quota::RefundQueue::<Runtime>::get().is_empty()); + assert_eq!(Balances::free_balance(Treasury::account_id()), 100 + 3); + }) +} diff --git a/runtime/gtest/Cargo.toml b/runtime/gtest/Cargo.toml index a76ac672d..1fcac6252 100644 --- a/runtime/gtest/Cargo.toml +++ b/runtime/gtest/Cargo.toml @@ -65,6 +65,7 @@ std = [ 'pallet-collective/std', 'pallet-distance/std', 'pallet-duniter-account/std', + 'pallet-quota/std', 'pallet-duniter-wot/std', 'pallet-grandpa/std', 'pallet-identity/std', @@ -94,6 +95,7 @@ std = [ 'sp-block-builder/std', 'sp-consensus-babe/std', 'sp-core/std', + 'sp-distance/std', 'sp-inherents/std', 'sp-offchain/std', 'sp-membership/std', @@ -137,16 +139,18 @@ pallet-authority-members = { path = '../../pallets/authority-members', default-f pallet-certification = { path = '../../pallets/certification', default-features = false } pallet-distance = { path = "../../pallets/distance", default-features = false } pallet-duniter-account = { path = '../../pallets/duniter-account', default-features = false } +pallet-quota = { path = '../../pallets/quota', default-features = false } pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false } pallet-identity = { path = '../../pallets/identity', default-features = false } pallet-membership = { path = '../../pallets/membership', default-features = false } +pallet-offences = { path = '../../pallets/offences', default-features = false } pallet-oneshot-account = { path = '../../pallets/oneshot-account', default-features = false } pallet-provide-randomness = { path = '../../pallets/provide-randomness', default-features = false } pallet-universal-dividend = { path = '../../pallets/universal-dividend', default-features = false } pallet-session-benchmarking = { path = '../../pallets/session-benchmarking', default-features = false } pallet-upgrade-origin = { path = '../../pallets/upgrade-origin', default-features = false } +sp-distance = { path = '../../primitives/distance', default-features = false } sp-membership = { path = '../../primitives/membership', default-features = false } -pallet-offences = { path = '../../pallets/offences', default-features = false } # crates.io codec = { package = "parity-scale-codec", version = "3.4.0", features = ["derive"], default-features = false } diff --git a/runtime/gtest/src/lib.rs b/runtime/gtest/src/lib.rs index 4d3b0e4a0..185c6abb1 100644 --- a/runtime/gtest/src/lib.rs +++ b/runtime/gtest/src/lib.rs @@ -52,7 +52,9 @@ use pallet_grandpa::fg_primitives; use pallet_grandpa::{AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList}; use sp_api::impl_runtime_apis; use sp_core::OpaqueMetadata; -use sp_runtime::traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, NumberFor, OpaqueKeys}; +use sp_runtime::traits::{ + AccountIdLookup, BlakeTwo256, Block as BlockT, NumberFor, One, OpaqueKeys, +}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, transaction_validity::{TransactionSource, TransactionValidity}, @@ -269,6 +271,7 @@ construct_runtime!( Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>} = 6, TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event<T>} = 32, OneshotAccount: pallet_oneshot_account::{Pallet, Call, Storage, Event<T>} = 7, + Quota: pallet_quota::{Pallet, Storage, Config<T>, Event<T>} = 66, // Consensus support AuthorityMembers: pallet_authority_members::{Pallet, Call, Storage, Config<T>, Event<T>} = 10, diff --git a/xtask/README.md b/xtask/README.md index 3f856c570..f9ee2d898 100644 --- a/xtask/README.md +++ b/xtask/README.md @@ -6,4 +6,21 @@ We choose [`xtask`](https://github.com/matklad/cargo-xtask/) to run Rust scripts cargo xtask # this will build the scripts and show the available commands ``` -These scripts mainly deal with runtime operations. \ No newline at end of file +These scripts mainly deal with runtime operations. + +## Doc + +``` +Usage: xtask <COMMAND> + +Commands: + build Build duniter binary + gen-calls-doc Generate calls documentation + inject-runtime-code Inject runtime code in raw specs + release-runtime Release a new runtime + test Execute unit tests and integration tests End2tests are skipped + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help +``` diff --git a/xtask/res/templates/runtime-calls-category.md b/xtask/res/templates/runtime-calls-category.md index d65ca060e..efe447ef4 100644 --- a/xtask/res/templates/runtime-calls-category.md +++ b/xtask/res/templates/runtime-calls-category.md @@ -20,7 +20,7 @@ There are **{{ calls_counter }}** {{ category_name }} calls from **{{ pallets | </details> {# replace markdown sytax in documentation breaking the final result #} -{{ call.documentation | replace(from="# WARNING:", to="WARNING:") }} +{{ call.documentation | replace(from="# WARNING:", to="WARNING:") | replace(from="## Complexity", to="**Complexity**") }} {% endfor -%} {% endfor -%} -- GitLab