From 23f537db5c76e9dd0607bded48b4fc005274cf9b Mon Sep 17 00:00:00 2001 From: girazoki Date: Thu, 15 Feb 2024 16:09:23 +0100 Subject: [PATCH 1/5] Refund address to services-payment (#406) * start working on refund address * more tests * runtime modifications * typescript changes * typescript test * fmt and take * bench * clean refund address always * refnd address as option * wait sessions 2 --- pallets/services-payment/src/benchmarks.rs | 21 +++- pallets/services-payment/src/lib.rs | 53 ++++++++- pallets/services-payment/src/mock.rs | 2 + pallets/services-payment/src/tests.rs | 112 +++++++++++++++++- pallets/services-payment/src/weights.rs | 27 +++++ runtime/dancebox/src/lib.rs | 2 + runtime/flashbox/src/lib.rs | 2 + ...e_payment_removes_tank_money_and_burns.ts} | 0 ..._payment_removes_tank_money_and_refunds.ts | 71 +++++++++++ .../parathreads/test_tanssi_parathreads.ts | 2 +- .../dancebox/interfaces/augment-api-events.ts | 5 + .../dancebox/interfaces/augment-api-query.ts | 7 ++ .../src/dancebox/interfaces/augment-api-tx.ts | 8 ++ .../src/dancebox/interfaces/lookup.ts | 8 ++ .../src/dancebox/interfaces/types-lookup.ts | 14 ++- .../flashbox/interfaces/augment-api-events.ts | 5 + .../flashbox/interfaces/augment-api-query.ts | 7 ++ .../src/flashbox/interfaces/augment-api-tx.ts | 8 ++ .../src/flashbox/interfaces/lookup.ts | 8 ++ .../src/flashbox/interfaces/types-lookup.ts | 14 ++- 20 files changed, 366 insertions(+), 10 deletions(-) rename test/suites/common-tanssi/services-payment/{test_service_payment_removes_tank_money.ts => test_service_payment_removes_tank_money_and_burns.ts} (100%) create mode 100644 test/suites/common-tanssi/services-payment/test_service_payment_removes_tank_money_and_refunds.ts diff --git a/pallets/services-payment/src/benchmarks.rs b/pallets/services-payment/src/benchmarks.rs index 5655a4f10..8a80128b8 100644 --- a/pallets/services-payment/src/benchmarks.rs +++ b/pallets/services-payment/src/benchmarks.rs @@ -22,7 +22,7 @@ use { frame_benchmarking::{account, v2::*}, frame_support::{ assert_ok, - traits::{Currency, Get}, + traits::{Currency, EnsureOriginWithArg, Get}, }, frame_system::RawOrigin, sp_runtime::Saturating, @@ -122,6 +122,25 @@ mod benchmarks { assert!(crate::GivenFreeCredits::::get(¶_id).is_some()); } + #[benchmark] + fn set_refund_address() { + let para_id = 1001u32.into(); + + let origin = T::SetRefundAddressOrigin::try_successful_origin(¶_id) + .expect("failed to create SetRefundAddressOrigin"); + + let refund_address = account("sufficient", 0, 1000); + + // Before call: no given free credits + assert!(crate::RefundAddress::::get(¶_id).is_none()); + + #[extrinsic_call] + Pallet::::set_refund_address(origin as T::RuntimeOrigin, para_id, Some(refund_address)); + + // After call: given free credits + assert!(crate::RefundAddress::::get(¶_id).is_some()); + } + #[benchmark] fn on_container_author_noted() { let para_id = 1001u32; diff --git a/pallets/services-payment/src/lib.rs b/pallets/services-payment/src/lib.rs index 6fa73ade4..c8d4430ac 100644 --- a/pallets/services-payment/src/lib.rs +++ b/pallets/services-payment/src/lib.rs @@ -41,7 +41,10 @@ use { frame_support::{ pallet_prelude::*, sp_runtime::{traits::Zero, Saturating}, - traits::{tokens::ExistenceRequirement, Currency, OnUnbalanced, WithdrawReasons}, + traits::{ + tokens::ExistenceRequirement, Currency, EnsureOriginWithArg, OnUnbalanced, + WithdrawReasons, + }, }, frame_system::pallet_prelude::*, scale_info::prelude::vec::Vec, @@ -76,6 +79,9 @@ pub mod pallet { /// Provider of a block cost which can adjust from block to block type ProvideBlockProductionCost: ProvideBlockProductionCost; + // Who can call set_refund_address? + type SetRefundAddressOrigin: EnsureOriginWithArg; + /// The maximum number of credits that can be accumulated type MaxCreditsStored: Get>; @@ -108,6 +114,10 @@ pub mod pallet { para_id: ParaId, credits: BlockNumberFor, }, + RefundAddressUpdated { + para_id: ParaId, + refund_address: Option, + }, } #[pallet::storage] @@ -120,6 +130,12 @@ pub mod pallet { #[pallet::getter(fn given_free_credits)] pub type GivenFreeCredits = StorageMap<_, Blake2_128Concat, ParaId, (), OptionQuery>; + /// Refund address + #[pallet::storage] + #[pallet::getter(fn refund_address)] + pub type RefundAddress = + StorageMap<_, Blake2_128Concat, ParaId, T::AccountId, OptionQuery>; + #[pallet::call] impl Pallet where @@ -191,6 +207,30 @@ pub mod pallet { Ok(().into()) } + + /// Call index to set the refund address for non-spent tokens + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::set_refund_address())] + pub fn set_refund_address( + origin: OriginFor, + para_id: ParaId, + refund_address: Option, + ) -> DispatchResultWithPostInfo { + T::SetRefundAddressOrigin::ensure_origin(origin, ¶_id)?; + + if let Some(refund_address) = refund_address.clone() { + RefundAddress::::insert(para_id, refund_address.clone()); + } else { + RefundAddress::::remove(para_id); + } + + Self::deposit_event(Event::::RefundAddressUpdated { + para_id, + refund_address, + }); + + Ok(().into()) + } } impl Pallet { @@ -332,11 +372,18 @@ impl Pallet { WithdrawReasons::FEE, ExistenceRequirement::AllowDeath, ) { - // Burn for now, we might be able to pass something to do with this - drop(imbalance); + if let Some(address) = RefundAddress::::get(para_id) { + T::Currency::resolve_creating(&address, imbalance); + } else { + // Burn for now, we might be able to pass something to do with this + drop(imbalance); + } } } + // Clean refund addres + RefundAddress::::remove(para_id); + // Clean credits BlockProductionCredits::::remove(para_id); } diff --git a/pallets/services-payment/src/mock.rs b/pallets/services-payment/src/mock.rs index 07a15aec8..8a66d86bf 100644 --- a/pallets/services-payment/src/mock.rs +++ b/pallets/services-payment/src/mock.rs @@ -36,6 +36,7 @@ use { parameter_types, traits::{ConstU32, ConstU64, Everything}, }, + frame_system::EnsureRoot, sp_core::H256, sp_runtime::{ traits::{BlakeTwo256, IdentityLookup}, @@ -112,6 +113,7 @@ impl pallet_services_payment::Config for Test { type OnChargeForBlock = (); type Currency = Balances; type ProvideBlockProductionCost = BlockProductionCost; + type SetRefundAddressOrigin = EnsureRoot; type MaxCreditsStored = MaxCreditsStored; type WeightInfo = (); } diff --git a/pallets/services-payment/src/tests.rs b/pallets/services-payment/src/tests.rs index 0b2ec9e41..2fca3d004 100644 --- a/pallets/services-payment/src/tests.rs +++ b/pallets/services-payment/src/tests.rs @@ -29,7 +29,7 @@ //! to that containerChain, by simply assigning the slot position. use { - crate::{mock::*, pallet as pallet_services_payment, BlockProductionCredits}, + crate::{mock::*, pallet as pallet_services_payment, BlockProductionCredits, RefundAddress}, cumulus_primitives_core::ParaId, frame_support::{assert_err, assert_ok, traits::fungible::Inspect}, sp_runtime::DispatchError, @@ -266,3 +266,113 @@ fn not_having_enough_tokens_in_tank_should_not_error() { ); }); } + +#[test] +fn on_deregister_burns_if_no_deposit_address() { + ExtBuilder::default() + .with_balances([(ALICE, 2_000)].into()) + .build() + .execute_with(|| { + // this should give 10 block credit + assert_ok!(PaymentServices::purchase_credits( + RuntimeOrigin::signed(ALICE), + 1.into(), + 1000u128, + )); + + let issuance_before = Balances::total_issuance(); + crate::Pallet::::para_deregistered(1.into()); + let issuance_after = Balances::total_issuance(); + assert_eq!(issuance_after, issuance_before - 1000u128); + + // Refund address gets cleared + assert!(>::get(ParaId::from(1)).is_none()); + }); +} + +#[test] +fn on_deregister_cleans_refund_address_even_when_purchases_have_not_being_made() { + ExtBuilder::default() + .with_balances([(ALICE, 2_000)].into()) + .build() + .execute_with(|| { + let refund_address = 10u64; + + assert_ok!(PaymentServices::set_refund_address( + RuntimeOrigin::root(), + 1.into(), + Some(refund_address), + )); + + crate::Pallet::::para_deregistered(1.into()); + + // Refund address gets cleared + assert!(>::get(ParaId::from(1)).is_none()); + }); +} + +#[test] +fn on_deregister_deposits_if_refund_address() { + ExtBuilder::default() + .with_balances([(ALICE, 2_000)].into()) + .build() + .execute_with(|| { + let refund_address = 10u64; + // this should give 10 block credit + assert_ok!(PaymentServices::purchase_credits( + RuntimeOrigin::signed(ALICE), + 1.into(), + 1000u128, + )); + + // this should set refund address + assert_ok!(PaymentServices::set_refund_address( + RuntimeOrigin::root(), + 1.into(), + Some(refund_address), + )); + + let issuance_before = Balances::total_issuance(); + crate::Pallet::::para_deregistered(1.into()); + let issuance_after = Balances::total_issuance(); + assert_eq!(issuance_after, issuance_before); + + let balance_refund_address = Balances::balance(&refund_address); + assert_eq!(balance_refund_address, 1000u128); + + assert!(>::get(ParaId::from(1)).is_none()); + }); +} + +#[test] +fn set_refund_address_with_none_removes_storage() { + ExtBuilder::default() + .with_balances([(ALICE, 2_000)].into()) + .build() + .execute_with(|| { + let refund_address = 10u64; + // this should give 10 block credit + assert_ok!(PaymentServices::purchase_credits( + RuntimeOrigin::signed(ALICE), + 1.into(), + 1000u128, + )); + + // this should set refund address + assert_ok!(PaymentServices::set_refund_address( + RuntimeOrigin::root(), + 1.into(), + Some(refund_address), + )); + + assert!(>::get(ParaId::from(1)).is_some()); + + assert_ok!(PaymentServices::set_refund_address( + RuntimeOrigin::root(), + 1.into(), + None, + )); + + assert!(>::get(ParaId::from(1)).is_none()); + }); +} diff --git a/pallets/services-payment/src/weights.rs b/pallets/services-payment/src/weights.rs index d28639959..c075ea08f 100644 --- a/pallets/services-payment/src/weights.rs +++ b/pallets/services-payment/src/weights.rs @@ -56,6 +56,7 @@ pub trait WeightInfo { fn set_credits() -> Weight; fn set_given_free_credits() -> Weight; fn on_container_author_noted() -> Weight; + fn set_refund_address() -> Weight; } /// Weights for pallet_services_payment using the Substrate node and recommended hardware. @@ -108,6 +109,19 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } + /// Storage: `Registrar::RegistrarDeposit` (r:1 w:0) + /// Proof: `Registrar::RegistrarDeposit` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ServicesPayment::RefundAddress` (r:0 w:1) + /// Proof: `ServicesPayment::RefundAddress` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + fn set_refund_address() -> Weight { + // Proof Size summary in bytes: + // Measured: `195` + // Estimated: `3660` + // Minimum execution time: 12_734_000 picoseconds. + Weight::from_parts(13_245_000, 3660) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } } // For backwards compatibility and tests @@ -158,4 +172,17 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + /// Storage: `Registrar::RegistrarDeposit` (r:1 w:0) + /// Proof: `Registrar::RegistrarDeposit` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `ServicesPayment::RefundAddress` (r:0 w:1) + /// Proof: `ServicesPayment::RefundAddress` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + fn set_refund_address() -> Weight { + // Proof Size summary in bytes: + // Measured: `195` + // Estimated: `3660` + // Minimum execution time: 12_734_000 picoseconds. + Weight::from_parts(13_245_000, 3660) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } } diff --git a/runtime/dancebox/src/lib.rs b/runtime/dancebox/src/lib.rs index 501b1cf48..a26ee28f9 100644 --- a/runtime/dancebox/src/lib.rs +++ b/runtime/dancebox/src/lib.rs @@ -845,6 +845,8 @@ impl pallet_services_payment::Config for Runtime { type Currency = Balances; /// Provider of a block cost which can adjust from block to block type ProvideBlockProductionCost = BlockProductionCost; + type SetRefundAddressOrigin = + EitherOfDiverse, EnsureRoot>; /// The maximum number of credits that can be accumulated type MaxCreditsStored = MaxCreditsStored; type WeightInfo = pallet_services_payment::weights::SubstrateWeight; diff --git a/runtime/flashbox/src/lib.rs b/runtime/flashbox/src/lib.rs index ce860c441..e1590faa9 100644 --- a/runtime/flashbox/src/lib.rs +++ b/runtime/flashbox/src/lib.rs @@ -707,6 +707,8 @@ impl pallet_services_payment::Config for Runtime { type Currency = Balances; /// Provider of a block cost which can adjust from block to block type ProvideBlockProductionCost = BlockProductionCost; + type SetRefundAddressOrigin = + EitherOfDiverse, EnsureRoot>; /// The maximum number of credits that can be accumulated type MaxCreditsStored = MaxCreditsStored; type WeightInfo = pallet_services_payment::weights::SubstrateWeight; diff --git a/test/suites/common-tanssi/services-payment/test_service_payment_removes_tank_money.ts b/test/suites/common-tanssi/services-payment/test_service_payment_removes_tank_money_and_burns.ts similarity index 100% rename from test/suites/common-tanssi/services-payment/test_service_payment_removes_tank_money.ts rename to test/suites/common-tanssi/services-payment/test_service_payment_removes_tank_money_and_burns.ts diff --git a/test/suites/common-tanssi/services-payment/test_service_payment_removes_tank_money_and_refunds.ts b/test/suites/common-tanssi/services-payment/test_service_payment_removes_tank_money_and_refunds.ts new file mode 100644 index 000000000..b70242b45 --- /dev/null +++ b/test/suites/common-tanssi/services-payment/test_service_payment_removes_tank_money_and_refunds.ts @@ -0,0 +1,71 @@ +import "@tanssi/api-augment"; +import { describeSuite, expect, beforeAll } from "@moonwall/cli"; +import { ApiPromise } from "@polkadot/api"; +import { KeyringPair, generateKeyringPair } from "@moonwall/util"; +import { jumpSessions } from "util/block"; +import { paraIdTank } from "util/payment"; + +describeSuite({ + id: "CT0604", + title: "Services payment test suite", + foundationMethods: "dev", + testCases: ({ it, context }) => { + let polkadotJs: ApiPromise; + let alice: KeyringPair; + const blocksPerSession = 5n; + const paraId2001 = 2001n; + const costPerBlock = 1_000_000n; + let refundAddress; + let balanceTankBefore; + let purchasedCredits; + beforeAll(async () => { + polkadotJs = context.polkadotJs(); + alice = context.keyring.alice; + refundAddress = generateKeyringPair("sr25519"); + const tx2001OneSession = polkadotJs.tx.servicesPayment.setCredits(paraId2001, 0); + await context.createBlock([await polkadotJs.tx.sudo.sudo(tx2001OneSession).signAsync(alice)]); + const existentialDeposit = await polkadotJs.consts.balances.existentialDeposit.toBigInt(); + // Now, buy some credits for container chain 2001 + purchasedCredits = blocksPerSession * costPerBlock + existentialDeposit; + const tx = polkadotJs.tx.servicesPayment.purchaseCredits(paraId2001, purchasedCredits); + await context.createBlock([await tx.signAsync(alice)]); + balanceTankBefore = (await polkadotJs.query.system.account(paraIdTank(paraId2001))).data.free.toBigInt(); + expect(balanceTankBefore, `Tank should have been filled`).toBe(purchasedCredits); + }); + it({ + id: "E01", + title: "Sudo can set refund address", + test: async function () { + // We deregister the chain + const setRefundAddress = polkadotJs.tx.sudo.sudo( + polkadotJs.tx.servicesPayment.setRefundAddress(paraId2001, refundAddress.address) + ); + await context.createBlock([await setRefundAddress.signAsync(alice)]); + // Check that we can fetch the address + const refundAddressOnChain = await polkadotJs.query.servicesPayment.refundAddress(paraId2001); + expect(refundAddressOnChain.toString(), `Refund address should be set`).toBe(refundAddress.address); + }, + }); + it({ + id: "E02", + title: "On deregistration we refund the address", + test: async function () { + // We deregister the chain + const deregister2001 = polkadotJs.tx.sudo.sudo(polkadotJs.tx.registrar.deregister(paraId2001)); + await context.createBlock([await deregister2001.signAsync(alice)]); + // Check that after 2 sessions, tank is empty and chain is deregistered + await jumpSessions(context, 2); + const balanceTank = ( + await polkadotJs.query.system.account(paraIdTank(paraId2001)) + ).data.free.toBigInt(); + expect(balanceTank, `Tank should have been removed`).toBe(0n); + + const balanceRefundAddress = ( + await polkadotJs.query.system.account(refundAddress.address) + ).data.free.toBigInt(); + + expect(balanceRefundAddress).toBe(purchasedCredits); + }, + }); + }, +}); diff --git a/test/suites/parathreads/test_tanssi_parathreads.ts b/test/suites/parathreads/test_tanssi_parathreads.ts index 8ed4e96c1..1eee85323 100644 --- a/test/suites/parathreads/test_tanssi_parathreads.ts +++ b/test/suites/parathreads/test_tanssi_parathreads.ts @@ -260,7 +260,7 @@ describeSuite({ timeout: 120000, test: async function () { // Wait 1 session so that parathreads have produced at least a few blocks each - await waitSessions(context, paraApi, 1); + await waitSessions(context, paraApi, 2); // TODO: calculate block frequency somehow assertSlotFrequency(await getBlockData(paraApi), 1); diff --git a/typescript-api/src/dancebox/interfaces/augment-api-events.ts b/typescript-api/src/dancebox/interfaces/augment-api-events.ts index 0800f750b..3acd55a06 100644 --- a/typescript-api/src/dancebox/interfaces/augment-api-events.ts +++ b/typescript-api/src/dancebox/interfaces/augment-api-events.ts @@ -943,6 +943,11 @@ declare module "@polkadot/api-base/types/events" { { paraId: u32; payer: AccountId32; credit: u128 } >; CreditsSet: AugmentedEvent; + RefundAddressUpdated: AugmentedEvent< + ApiType, + [paraId: u32, refundAddress: Option], + { paraId: u32; refundAddress: Option } + >; /** Generic event */ [key: string]: AugmentedEvent; }; diff --git a/typescript-api/src/dancebox/interfaces/augment-api-query.ts b/typescript-api/src/dancebox/interfaces/augment-api-query.ts index af7c1d627..45a4ae1ba 100644 --- a/typescript-api/src/dancebox/interfaces/augment-api-query.ts +++ b/typescript-api/src/dancebox/interfaces/augment-api-query.ts @@ -938,6 +938,13 @@ declare module "@polkadot/api-base/types/storage" { [u32] > & QueryableStorageEntry; + /** Refund address */ + refundAddress: AugmentedQuery< + ApiType, + (arg: u32 | AnyNumber | Uint8Array) => Observable>, + [u32] + > & + QueryableStorageEntry; /** Generic query */ [key: string]: QueryableStorageEntry; }; diff --git a/typescript-api/src/dancebox/interfaces/augment-api-tx.ts b/typescript-api/src/dancebox/interfaces/augment-api-tx.ts index e90418c68..1b73e45a3 100644 --- a/typescript-api/src/dancebox/interfaces/augment-api-tx.ts +++ b/typescript-api/src/dancebox/interfaces/augment-api-tx.ts @@ -1583,6 +1583,14 @@ declare module "@polkadot/api-base/types/submittable" { ) => SubmittableExtrinsic, [u32, bool] >; + /** See [`Pallet::set_refund_address`]. */ + setRefundAddress: AugmentedSubmittable< + ( + paraId: u32 | AnyNumber | Uint8Array, + refundAddress: Option | null | Uint8Array | AccountId32 | string + ) => SubmittableExtrinsic, + [u32, Option] + >; /** Generic tx */ [key: string]: SubmittableExtrinsicFunction; }; diff --git a/typescript-api/src/dancebox/interfaces/lookup.ts b/typescript-api/src/dancebox/interfaces/lookup.ts index a08cef33c..f122e59a1 100644 --- a/typescript-api/src/dancebox/interfaces/lookup.ts +++ b/typescript-api/src/dancebox/interfaces/lookup.ts @@ -488,6 +488,10 @@ export default { paraId: "u32", credits: "u32", }, + RefundAddressUpdated: { + paraId: "u32", + refundAddress: "Option", + }, }, }, /** Lookup55: pallet_data_preservers::pallet::Event */ @@ -2375,6 +2379,10 @@ export default { paraId: "u32", givenFreeCredits: "bool", }, + set_refund_address: { + paraId: "u32", + refundAddress: "Option", + }, }, }, /** Lookup264: pallet_data_preservers::pallet::Call */ diff --git a/typescript-api/src/dancebox/interfaces/types-lookup.ts b/typescript-api/src/dancebox/interfaces/types-lookup.ts index ab63c25ef..8c910cff0 100644 --- a/typescript-api/src/dancebox/interfaces/types-lookup.ts +++ b/typescript-api/src/dancebox/interfaces/types-lookup.ts @@ -722,7 +722,12 @@ declare module "@polkadot/types/lookup" { readonly paraId: u32; readonly credits: u32; } & Struct; - readonly type: "CreditsPurchased" | "CreditBurned" | "CreditsSet"; + readonly isRefundAddressUpdated: boolean; + readonly asRefundAddressUpdated: { + readonly paraId: u32; + readonly refundAddress: Option; + } & Struct; + readonly type: "CreditsPurchased" | "CreditBurned" | "CreditsSet" | "RefundAddressUpdated"; } /** @name PalletDataPreserversEvent (55) */ @@ -3199,7 +3204,12 @@ declare module "@polkadot/types/lookup" { readonly paraId: u32; readonly givenFreeCredits: bool; } & Struct; - readonly type: "PurchaseCredits" | "SetCredits" | "SetGivenFreeCredits"; + readonly isSetRefundAddress: boolean; + readonly asSetRefundAddress: { + readonly paraId: u32; + readonly refundAddress: Option; + } & Struct; + readonly type: "PurchaseCredits" | "SetCredits" | "SetGivenFreeCredits" | "SetRefundAddress"; } /** @name PalletDataPreserversCall (264) */ diff --git a/typescript-api/src/flashbox/interfaces/augment-api-events.ts b/typescript-api/src/flashbox/interfaces/augment-api-events.ts index 0d1dd6f1f..d0b8aca61 100644 --- a/typescript-api/src/flashbox/interfaces/augment-api-events.ts +++ b/typescript-api/src/flashbox/interfaces/augment-api-events.ts @@ -354,6 +354,11 @@ declare module "@polkadot/api-base/types/events" { { paraId: u32; payer: AccountId32; credit: u128 } >; CreditsSet: AugmentedEvent; + RefundAddressUpdated: AugmentedEvent< + ApiType, + [paraId: u32, refundAddress: Option], + { paraId: u32; refundAddress: Option } + >; /** Generic event */ [key: string]: AugmentedEvent; }; diff --git a/typescript-api/src/flashbox/interfaces/augment-api-query.ts b/typescript-api/src/flashbox/interfaces/augment-api-query.ts index ae6e19aa6..2ca919181 100644 --- a/typescript-api/src/flashbox/interfaces/augment-api-query.ts +++ b/typescript-api/src/flashbox/interfaces/augment-api-query.ts @@ -638,6 +638,13 @@ declare module "@polkadot/api-base/types/storage" { [u32] > & QueryableStorageEntry; + /** Refund address */ + refundAddress: AugmentedQuery< + ApiType, + (arg: u32 | AnyNumber | Uint8Array) => Observable>, + [u32] + > & + QueryableStorageEntry; /** Generic query */ [key: string]: QueryableStorageEntry; }; diff --git a/typescript-api/src/flashbox/interfaces/augment-api-tx.ts b/typescript-api/src/flashbox/interfaces/augment-api-tx.ts index fe43b5462..1991adbe9 100644 --- a/typescript-api/src/flashbox/interfaces/augment-api-tx.ts +++ b/typescript-api/src/flashbox/interfaces/augment-api-tx.ts @@ -861,6 +861,14 @@ declare module "@polkadot/api-base/types/submittable" { ) => SubmittableExtrinsic, [u32, bool] >; + /** See [`Pallet::set_refund_address`]. */ + setRefundAddress: AugmentedSubmittable< + ( + paraId: u32 | AnyNumber | Uint8Array, + refundAddress: Option | null | Uint8Array | AccountId32 | string + ) => SubmittableExtrinsic, + [u32, Option] + >; /** Generic tx */ [key: string]: SubmittableExtrinsicFunction; }; diff --git a/typescript-api/src/flashbox/interfaces/lookup.ts b/typescript-api/src/flashbox/interfaces/lookup.ts index 474c11ca7..5a93b0a91 100644 --- a/typescript-api/src/flashbox/interfaces/lookup.ts +++ b/typescript-api/src/flashbox/interfaces/lookup.ts @@ -488,6 +488,10 @@ export default { paraId: "u32", credits: "u32", }, + RefundAddressUpdated: { + paraId: "u32", + refundAddress: "Option", + }, }, }, /** Lookup55: pallet_data_preservers::pallet::Event */ @@ -1308,6 +1312,10 @@ export default { paraId: "u32", givenFreeCredits: "bool", }, + set_refund_address: { + paraId: "u32", + refundAddress: "Option", + }, }, }, /** Lookup203: pallet_data_preservers::pallet::Call */ diff --git a/typescript-api/src/flashbox/interfaces/types-lookup.ts b/typescript-api/src/flashbox/interfaces/types-lookup.ts index b04496346..d9c5a3f00 100644 --- a/typescript-api/src/flashbox/interfaces/types-lookup.ts +++ b/typescript-api/src/flashbox/interfaces/types-lookup.ts @@ -722,7 +722,12 @@ declare module "@polkadot/types/lookup" { readonly paraId: u32; readonly credits: u32; } & Struct; - readonly type: "CreditsPurchased" | "CreditBurned" | "CreditsSet"; + readonly isRefundAddressUpdated: boolean; + readonly asRefundAddressUpdated: { + readonly paraId: u32; + readonly refundAddress: Option; + } & Struct; + readonly type: "CreditsPurchased" | "CreditBurned" | "CreditsSet" | "RefundAddressUpdated"; } /** @name PalletDataPreserversEvent (55) */ @@ -1704,7 +1709,12 @@ declare module "@polkadot/types/lookup" { readonly paraId: u32; readonly givenFreeCredits: bool; } & Struct; - readonly type: "PurchaseCredits" | "SetCredits" | "SetGivenFreeCredits"; + readonly isSetRefundAddress: boolean; + readonly asSetRefundAddress: { + readonly paraId: u32; + readonly refundAddress: Option; + } & Struct; + readonly type: "PurchaseCredits" | "SetCredits" | "SetGivenFreeCredits" | "SetRefundAddress"; } /** @name PalletDataPreserversCall (203) */ From d28aca5af7aa24d7c421242b549b83b9e3e54584 Mon Sep 17 00:00:00 2001 From: tmpolaczyk <44604217+tmpolaczyk@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:17:34 +0100 Subject: [PATCH 2/5] Always use wasm executor in container collators (#412) --- client/node-common/src/service.rs | 46 ++++++++++--- .../templates/frontier/node/src/service.rs | 2 +- .../templates/simple/node/src/service.rs | 2 +- node/src/container_chain_monitor.rs | 6 +- node/src/service.rs | 69 +++++++++++++++---- 5 files changed, 99 insertions(+), 26 deletions(-) diff --git a/client/node-common/src/service.rs b/client/node-common/src/service.rs index 531e37d05..7fb13e662 100644 --- a/client/node-common/src/service.rs +++ b/client/node-common/src/service.rs @@ -35,8 +35,9 @@ use { run_manual_seal, ConsensusDataProvider, EngineCommand, ManualSealParams, }, sc_executor::{ - HeapAllocStrategy, NativeElseWasmExecutor, NativeExecutionDispatch, WasmExecutor, - DEFAULT_HEAP_ALLOC_STRATEGY, + sp_wasm_interface::{ExtendedHostFunctions, HostFunctions}, + HeapAllocStrategy, NativeElseWasmExecutor, NativeExecutionDispatch, RuntimeVersionOf, + WasmExecutor, DEFAULT_HEAP_ALLOC_STRATEGY, }, sc_network::{config::FullNetworkConfiguration, NetworkBlock, NetworkService}, sc_network_sync::SyncingService, @@ -52,6 +53,7 @@ use { sp_api::ConstructRuntimeApi, sp_block_builder::BlockBuilder, sp_consensus::SelectChain, + sp_core::traits::CodeExecutor, sp_inherents::CreateInherentDataProviders, sp_offchain::OffchainWorkerApi, sp_runtime::Percent, @@ -64,7 +66,7 @@ use { pub trait NodeBuilderConfig { type Block; type RuntimeApi; - type ParachainNativeExecutor; + type ParachainExecutor; /// Create a new `NodeBuilder` using the types of this `Config`, along /// with the parachain `Configuration` and an optional `HwBench`. @@ -75,7 +77,8 @@ pub trait NodeBuilderConfig { where Self: Sized, BlockOf: cumulus_primitives_core::BlockT, - ParachainNativeExecutorOf: NativeExecutionDispatch + 'static, + ExecutorOf: + Clone + CodeExecutor + RuntimeVersionOf + TanssiExecutorExt + Sync + Send + 'static, RuntimeApiOf: ConstructRuntimeApi, ClientOf> + Sync + Send + 'static, ConstructedRuntimeApiOf: @@ -89,8 +92,7 @@ pub type BlockOf = ::Block; pub type BlockHashOf = as cumulus_primitives_core::BlockT>::Hash; pub type BlockHeaderOf = as cumulus_primitives_core::BlockT>::Header; pub type RuntimeApiOf = ::RuntimeApi; -pub type ParachainNativeExecutorOf = ::ParachainNativeExecutor; -pub type ExecutorOf = NativeElseWasmExecutor>; +pub type ExecutorOf = ::ParachainExecutor; pub type ClientOf = TFullClient, RuntimeApiOf, ExecutorOf>; pub type BackendOf = TFullBackend>; pub type ConstructedRuntimeApiOf = @@ -130,7 +132,7 @@ pub struct NodeBuilder< SImportQueueService = (), > where BlockOf: cumulus_primitives_core::BlockT, - ParachainNativeExecutorOf: NativeExecutionDispatch + 'static, + ExecutorOf: Clone + CodeExecutor + RuntimeVersionOf + Sync + Send + 'static, RuntimeApiOf: ConstructRuntimeApi, ClientOf> + Sync + Send + 'static, ConstructedRuntimeApiOf: TaggedTransactionQueue> + BlockBuilder>, { @@ -157,13 +159,39 @@ pub struct Network { pub sync_service: Arc>, } +/// Allows to create a parachain-defined executor from a `WasmExecutor` +pub trait TanssiExecutorExt { + type HostFun: HostFunctions; + fn new_with_wasm_executor(wasm_executor: WasmExecutor) -> Self; +} + +impl TanssiExecutorExt for WasmExecutor { + type HostFun = sp_io::SubstrateHostFunctions; + + fn new_with_wasm_executor(wasm_executor: WasmExecutor) -> Self { + wasm_executor + } +} + +impl TanssiExecutorExt for NativeElseWasmExecutor +where + D: NativeExecutionDispatch, +{ + type HostFun = ExtendedHostFunctions; + + fn new_with_wasm_executor(wasm_executor: WasmExecutor) -> Self { + NativeElseWasmExecutor::new_with_wasm_executor(wasm_executor) + } +} + // `new` function doesn't take self, and the Rust compiler cannot infer that // only one type T implements `TypeIdentity`. With thus need a separate impl // block with concrete types `()`. impl NodeBuilder where BlockOf: cumulus_primitives_core::BlockT, - ParachainNativeExecutorOf: NativeExecutionDispatch + 'static, + ExecutorOf: + Clone + CodeExecutor + RuntimeVersionOf + TanssiExecutorExt + Sync + Send + 'static, RuntimeApiOf: ConstructRuntimeApi, ClientOf> + Sync + Send + 'static, ConstructedRuntimeApiOf: TaggedTransactionQueue> + BlockBuilder>, { @@ -255,7 +283,7 @@ impl NodeBuilder where BlockOf: cumulus_primitives_core::BlockT, - ParachainNativeExecutorOf: NativeExecutionDispatch + 'static, + ExecutorOf: Clone + CodeExecutor + RuntimeVersionOf + Sync + Send + 'static, RuntimeApiOf: ConstructRuntimeApi, ClientOf> + Sync + Send + 'static, ConstructedRuntimeApiOf: TaggedTransactionQueue> + BlockBuilder> diff --git a/container-chains/templates/frontier/node/src/service.rs b/container-chains/templates/frontier/node/src/service.rs index d209e1fd3..56733c83a 100644 --- a/container-chains/templates/frontier/node/src/service.rs +++ b/container-chains/templates/frontier/node/src/service.rs @@ -60,7 +60,7 @@ pub struct NodeConfig; impl NodeBuilderConfig for NodeConfig { type Block = Block; type RuntimeApi = RuntimeApi; - type ParachainNativeExecutor = TemplateRuntimeExecutor; + type ParachainExecutor = ParachainExecutor; } pub fn frontier_database_dir(config: &Configuration, path: &str) -> std::path::PathBuf { diff --git a/container-chains/templates/simple/node/src/service.rs b/container-chains/templates/simple/node/src/service.rs index f71132414..fc7463c3e 100644 --- a/container-chains/templates/simple/node/src/service.rs +++ b/container-chains/templates/simple/node/src/service.rs @@ -65,7 +65,7 @@ pub struct NodeConfig; impl NodeBuilderConfig for NodeConfig { type Block = Block; type RuntimeApi = RuntimeApi; - type ParachainNativeExecutor = ParachainNativeExecutor; + type ParachainExecutor = ParachainExecutor; } thread_local!(static TIMESTAMP: std::cell::RefCell = std::cell::RefCell::new(0)); diff --git a/node/src/container_chain_monitor.rs b/node/src/container_chain_monitor.rs index e66f8b99a..e2b8b613b 100644 --- a/node/src/container_chain_monitor.rs +++ b/node/src/container_chain_monitor.rs @@ -17,7 +17,7 @@ use { crate::{ container_chain_spawner::{CcSpawnMsg, ContainerChainSpawnerState}, - service::{ParachainBackend, ParachainClient}, + service::{ContainerChainBackend, ContainerChainClient}, }, cumulus_primitives_core::ParaId, std::{ @@ -57,9 +57,9 @@ pub struct SpawnedContainer { /// This won't be precise because it is checked using polling with a high period. pub stop_refcount_time: Cell>, /// Used to check the reference count, if it's 0 it means the database has been closed - pub backend: std::sync::Weak, + pub backend: std::sync::Weak, /// Used to check the reference count, if it's 0 it means that the client has been closed. - pub client: std::sync::Weak, + pub client: std::sync::Weak, } impl SpawnedContainer { diff --git a/node/src/service.rs b/node/src/service.rs index 79bcda71e..5e5ebf070 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -60,7 +60,7 @@ use { }, sc_consensus::BlockImport, sc_consensus::{BasicQueue, ImportQueue}, - sc_executor::NativeElseWasmExecutor, + sc_executor::{NativeElseWasmExecutor, WasmExecutor}, sc_network::NetworkBlock, sc_network_sync::SyncingService, sc_service::{Configuration, SpawnTaskHandle, TFullBackend, TFullClient, TaskManager}, @@ -101,19 +101,32 @@ pub struct NodeConfig; impl NodeBuilderConfig for NodeConfig { type Block = Block; type RuntimeApi = RuntimeApi; - type ParachainNativeExecutor = ParachainNativeExecutor; + type ParachainExecutor = ParachainExecutor; } -type ParachainExecutor = NativeElseWasmExecutor; +pub struct ContainerChainNodeConfig; +impl NodeBuilderConfig for ContainerChainNodeConfig { + type Block = Block; + // TODO: RuntimeApi here should be the subset of runtime apis available for all containers + // Currently we are using the orchestrator runtime apis + type RuntimeApi = RuntimeApi; + type ParachainExecutor = ContainerChainExecutor; +} +// Orchestrator chain types +type ParachainExecutor = NativeElseWasmExecutor; pub type ParachainClient = TFullClient; - pub type ParachainBackend = TFullBackend; - type DevParachainBlockImport = OrchestratorParachainBlockImport>; - type ParachainBlockImport = TParachainBlockImport, ParachainBackend>; +// Container chains types +type ContainerChainExecutor = WasmExecutor; +pub type ContainerChainClient = TFullClient; +pub type ContainerChainBackend = ParachainBackend; +type ContainerChainBlockImport = + TParachainBlockImport, ContainerChainBackend>; + thread_local!(static TIMESTAMP: std::cell::RefCell = std::cell::RefCell::new(0)); /// Provide a mock duration starting at 0 in millisecond for timestamp inherent. @@ -242,6 +255,33 @@ pub fn import_queue( (block_import, import_queue) } +pub fn container_chain_import_queue( + parachain_config: &Configuration, + node_builder: &NodeBuilder, +) -> (ContainerChainBlockImport, BasicQueue) { + // The nimbus import queue ONLY checks the signature correctness + // Any other checks corresponding to the author-correctness should be done + // in the runtime + let block_import = + ContainerChainBlockImport::new(node_builder.client.clone(), node_builder.backend.clone()); + + let import_queue = nimbus_consensus::import_queue( + node_builder.client.clone(), + block_import.clone(), + move |_, _| async move { + let time = sp_timestamp::InherentDataProvider::from_system_time(); + + Ok((time,)) + }, + &node_builder.task_manager.spawn_essential_handle(), + parachain_config.prometheus_registry(), + false, + ) + .expect("function never fails"); + + (block_import, import_queue) +} + /// Start a node with the given parachain `Configuration` and relay chain `Configuration`. /// /// This is the actual implementation that is abstract over the executor and the runtime api. @@ -469,13 +509,18 @@ pub async fn start_node_impl_container( para_id: ParaId, orchestrator_para_id: ParaId, collator: bool, -) -> sc_service::error::Result<(TaskManager, Arc, Arc)> { +) -> sc_service::error::Result<( + TaskManager, + Arc, + Arc, +)> { let parachain_config = prepare_node_config(parachain_config); // Create a `NodeBuilder` which helps setup parachain nodes common systems. - let node_builder = NodeConfig::new_builder(¶chain_config, None)?; + let node_builder = ContainerChainNodeConfig::new_builder(¶chain_config, None)?; - let (block_import, import_queue) = import_queue(¶chain_config, &node_builder); + let (block_import, import_queue) = + container_chain_import_queue(¶chain_config, &node_builder); let import_queue_service = import_queue.service(); log::info!("are we collators? {:?}", collator); @@ -594,15 +639,15 @@ fn build_manual_seal_import_queue( #[sc_tracing::logging::prefix_logs_with(container_log_str(para_id))] fn start_consensus_container( - client: Arc, + client: Arc, orchestrator_client: Arc, - block_import: ParachainBlockImport, + block_import: ContainerChainBlockImport, prometheus_registry: Option, telemetry: Option, spawner: SpawnTaskHandle, relay_chain_interface: Arc, orchestrator_chain_interface: Arc, - transaction_pool: Arc>, + transaction_pool: Arc>, sync_oracle: Arc>, keystore: KeystorePtr, force_authoring: bool, From 90433bbb93aa8a9aa822df6ef57061a11d0e5062 Mon Sep 17 00:00:00 2001 From: nanocryk <6422796+nanocryk@users.noreply.github.com> Date: Fri, 16 Feb 2024 15:46:57 +0100 Subject: [PATCH 3/5] Stream Payment pallet (#391) * pallet layout * open stream + update logic * close stream + events * refill + change rate * adapter for fungible impl * rework pallet to not use fungibles traits * mock + some tests * more tests * refactor stream to prepare improved change requests * refactor + allow to request bigger changes * crate doc * clippy + skip 0 payment + prevent funds stuck on overflow * payment == deposit => drained * cleanup tests * rework deposit change + finish config change * update tests * wip bench * fmt * switch to holds * merge change_deposit into request_change/accept_requested_change + cancel_change_request * cleanup tests * more tests * update last_time_updated when changing time unit * refactor possible immediate change * immediate deposit change * more tests * fix CI * support deadline in past * for benches (wip) * fix rustfmt issue * mock ready for running benchmarks as tests * all benches and weights * fmt * update docs * add pallet to flashbox * typescript api * add integration tests * fix test * fmt * clippy --------- Co-authored-by: girazoki --- Cargo.lock | 26 + Cargo.toml | 3 + client/consensus/src/collators.rs | 58 +- client/consensus/src/collators/basic.rs | 63 +- .../consensus/src/consensus_orchestrator.rs | 4 +- client/consensus/src/lib.rs | 16 +- pallets/collator-assignment/src/assignment.rs | 6 +- pallets/registrar/src/lib.rs | 3 +- pallets/stream-payment/Cargo.toml | 66 + pallets/stream-payment/README.md | 57 + pallets/stream-payment/src/benchmarking.rs | 437 ++++ pallets/stream-payment/src/lib.rs | 938 +++++++++ pallets/stream-payment/src/mock.rs | 476 +++++ pallets/stream-payment/src/tests.rs | 1774 +++++++++++++++++ pallets/stream-payment/src/weights.rs | 317 +++ runtime/dancebox/Cargo.toml | 3 + runtime/dancebox/src/lib.rs | 151 +- runtime/dancebox/tests/integration_test.rs | 72 +- runtime/flashbox/Cargo.toml | 3 + runtime/flashbox/src/lib.rs | 139 +- .../stream-payment/test_stream_payment.ts | 106 + .../dancebox/interfaces/augment-api-errors.ts | 19 + .../dancebox/interfaces/augment-api-events.ts | 46 + .../dancebox/interfaces/augment-api-query.ts | 72 +- .../src/dancebox/interfaces/augment-api-tx.ts | 100 + .../src/dancebox/interfaces/lookup.ts | 643 +++--- .../src/dancebox/interfaces/registry.ts | 34 +- .../src/dancebox/interfaces/types-lookup.ts | 684 ++++--- .../flashbox/interfaces/augment-api-errors.ts | 19 + .../flashbox/interfaces/augment-api-events.ts | 48 +- .../flashbox/interfaces/augment-api-query.ts | 42 + .../src/flashbox/interfaces/augment-api-tx.ts | 100 + .../src/flashbox/interfaces/lookup.ts | 428 ++-- .../src/flashbox/interfaces/registry.ts | 24 + .../src/flashbox/interfaces/types-lookup.ts | 463 +++-- 35 files changed, 6603 insertions(+), 837 deletions(-) create mode 100644 pallets/stream-payment/Cargo.toml create mode 100644 pallets/stream-payment/README.md create mode 100644 pallets/stream-payment/src/benchmarking.rs create mode 100644 pallets/stream-payment/src/lib.rs create mode 100644 pallets/stream-payment/src/mock.rs create mode 100644 pallets/stream-payment/src/tests.rs create mode 100644 pallets/stream-payment/src/weights.rs create mode 100644 test/suites/dev-tanssi/stream-payment/test_stream_payment.ts diff --git a/Cargo.lock b/Cargo.lock index 53f7d446e..030d59fb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2854,6 +2854,7 @@ dependencies = [ "pallet-services-payment", "pallet-session", "pallet-staking", + "pallet-stream-payment", "pallet-sudo", "pallet-timestamp", "pallet-transaction-payment", @@ -3987,6 +3988,7 @@ dependencies = [ "pallet-root-testing", "pallet-services-payment", "pallet-session", + "pallet-stream-payment", "pallet-sudo", "pallet-timestamp", "pallet-transaction-payment", @@ -8729,6 +8731,30 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-stream-payment" +version = "0.1.0" +dependencies = [ + "dp-core", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "num-traits", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "serde", + "similar-asserts", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "tap", + "tp-maths", + "tp-traits", +] + [[package]] name = "pallet-sudo" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 77278b158..3a35b80c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ pallet-pooled-staking = { path = "pallets/pooled-staking", default-features = fa pallet-registrar = { path = "pallets/registrar", default-features = false } pallet-registrar-runtime-api = { path = "pallets/registrar/rpc/runtime-api", default-features = false } pallet-services-payment = { path = "pallets/services-payment", default-features = false } +pallet-stream-payment = { path = "pallets/stream-payment", default-features = false } container-chain-template-frontier-runtime = { path = "container-chains/templates/frontier/runtime", default-features = false } container-chain-template-simple-runtime = { path = "container-chains/templates/simple/runtime", default-features = false } @@ -47,6 +48,7 @@ tc-consensus = { path = "client/consensus" } tp-author-noting-inherent = { path = "primitives/author-noting-inherent", default-features = false } tp-consensus = { path = "primitives/consensus", default-features = false } tp-container-chain-genesis-data = { path = "primitives/container-chain-genesis-data", default-features = false } +tp-fungibles-ext = { path = "primitives/fungibles-ext", default-features = false } tp-maths = { path = "primitives/maths", default-features = false } tp-traits = { path = "primitives/traits", default-features = false } @@ -251,6 +253,7 @@ num_enum = { version = "0.7.1", default-features = false } rand_chacha = { version = "0.3.1", default-features = false } serde = { version = "1.0.152", default-features = false } smallvec = "1.10.0" +tap = "1.0.1" # General (client) async-io = "1.3" diff --git a/client/consensus/src/collators.rs b/client/consensus/src/collators.rs index fdb6eb839..725e27939 100644 --- a/client/consensus/src/collators.rs +++ b/client/consensus/src/collators.rs @@ -16,36 +16,36 @@ pub mod basic; -use cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface; -use cumulus_client_consensus_common::ParachainCandidate; -use cumulus_client_consensus_proposer::ProposerInterface; -use cumulus_primitives_core::{ - relay_chain::Hash as PHash, DigestItem, ParachainBlockData, PersistedValidationData, +use { + crate::{find_pre_digest, AuthorityId, OrchestratorAuraWorkerAuxData}, + cumulus_client_collator::service::ServiceInterface as CollatorServiceInterface, + cumulus_client_consensus_common::ParachainCandidate, + cumulus_client_consensus_proposer::ProposerInterface, + cumulus_primitives_core::{ + relay_chain::Hash as PHash, DigestItem, ParachainBlockData, PersistedValidationData, + }, + cumulus_primitives_parachain_inherent::ParachainInherentData, + cumulus_relay_chain_interface::RelayChainInterface, + futures::prelude::*, + nimbus_primitives::{CompatibleDigestItem as NimbusCompatibleDigestItem, NIMBUS_KEY_ID}, + parity_scale_codec::{Codec, Encode}, + polkadot_node_primitives::{Collation, MaybeCompressedPoV}, + polkadot_primitives::Id as ParaId, + sc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy, StateAction}, + sp_application_crypto::{AppCrypto, AppPublic}, + sp_consensus::BlockOrigin, + sp_consensus_aura::{digests::CompatibleDigestItem, Slot}, + sp_core::crypto::{ByteArray, Pair}, + sp_inherents::{CreateInherentDataProviders, InherentData, InherentDataProvider}, + sp_keystore::{Keystore, KeystorePtr}, + sp_runtime::{ + generic::Digest, + traits::{Block as BlockT, HashingFor, Header as HeaderT, Member, Zero}, + }, + sp_state_machine::StorageChanges, + sp_timestamp::Timestamp, + std::{convert::TryFrom, error::Error, time::Duration}, }; -use cumulus_primitives_parachain_inherent::ParachainInherentData; -use cumulus_relay_chain_interface::RelayChainInterface; -use parity_scale_codec::{Codec, Encode}; - -use polkadot_node_primitives::{Collation, MaybeCompressedPoV}; -use polkadot_primitives::Id as ParaId; - -use crate::{find_pre_digest, AuthorityId, OrchestratorAuraWorkerAuxData}; -use futures::prelude::*; -use nimbus_primitives::{CompatibleDigestItem as NimbusCompatibleDigestItem, NIMBUS_KEY_ID}; -use sc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy, StateAction}; -use sp_application_crypto::{AppCrypto, AppPublic}; -use sp_consensus::BlockOrigin; -use sp_consensus_aura::{digests::CompatibleDigestItem, Slot}; -use sp_core::crypto::{ByteArray, Pair}; -use sp_inherents::{CreateInherentDataProviders, InherentData, InherentDataProvider}; -use sp_keystore::{Keystore, KeystorePtr}; -use sp_runtime::{ - generic::Digest, - traits::{Block as BlockT, HashingFor, Header as HeaderT, Member, Zero}, -}; -use sp_state_machine::StorageChanges; -use sp_timestamp::Timestamp; -use std::{convert::TryFrom, error::Error, time::Duration}; /// Parameters for instantiating a [`Collator`]. pub struct Params { diff --git a/client/consensus/src/collators/basic.rs b/client/consensus/src/collators/basic.rs index b5999b69b..c51508d78 100644 --- a/client/consensus/src/collators/basic.rs +++ b/client/consensus/src/collators/basic.rs @@ -14,38 +14,39 @@ // You should have received a copy of the GNU General Public License // along with Tanssi. If not, see . -use cumulus_client_collator::{ - relay_chain_driven::CollationRequest, service::ServiceInterface as CollatorServiceInterface, +use { + crate::{ + collators as collator_util, consensus_orchestrator::RetrieveAuthoritiesFromOrchestrator, + OrchestratorAuraWorkerAuxData, + }, + cumulus_client_collator::{ + relay_chain_driven::CollationRequest, service::ServiceInterface as CollatorServiceInterface, + }, + cumulus_client_consensus_proposer::ProposerInterface, + cumulus_primitives_core::{ + relay_chain::{BlockId as RBlockId, Hash as PHash}, + PersistedValidationData, + }, + cumulus_relay_chain_interface::RelayChainInterface, + futures::{channel::mpsc::Receiver, prelude::*}, + parity_scale_codec::{Codec, Decode}, + polkadot_node_primitives::CollationResult, + polkadot_overseer::Handle as OverseerHandle, + polkadot_primitives::{CollatorPair, Id as ParaId}, + sc_client_api::{backend::AuxStore, BlockBackend, BlockOf}, + sc_consensus::BlockImport, + sc_consensus_slots::InherentDataProviderExt, + sp_api::ProvideRuntimeApi, + sp_application_crypto::AppPublic, + sp_blockchain::HeaderBackend, + sp_consensus::SyncOracle, + sp_consensus_aura::SlotDuration, + sp_core::crypto::Pair, + sp_inherents::CreateInherentDataProviders, + sp_keystore::KeystorePtr, + sp_runtime::traits::{Block as BlockT, Header as HeaderT, Member}, + std::{convert::TryFrom, sync::Arc, time::Duration}, }; -use cumulus_client_consensus_proposer::ProposerInterface; -use cumulus_primitives_core::{ - relay_chain::{BlockId as RBlockId, Hash as PHash}, - PersistedValidationData, -}; -use cumulus_relay_chain_interface::RelayChainInterface; -use parity_scale_codec::{Codec, Decode}; - -use polkadot_node_primitives::CollationResult; -use polkadot_overseer::Handle as OverseerHandle; -use polkadot_primitives::{CollatorPair, Id as ParaId}; - -use futures::{channel::mpsc::Receiver, prelude::*}; -use sc_client_api::{backend::AuxStore, BlockBackend, BlockOf}; -use sc_consensus::BlockImport; -use sc_consensus_slots::InherentDataProviderExt; -use sp_api::ProvideRuntimeApi; -use sp_application_crypto::AppPublic; -use sp_blockchain::HeaderBackend; -use sp_consensus::SyncOracle; -use sp_consensus_aura::SlotDuration; -use sp_core::crypto::Pair; -use sp_inherents::CreateInherentDataProviders; -use sp_keystore::KeystorePtr; -use sp_runtime::traits::{Block as BlockT, Header as HeaderT, Member}; -use std::{convert::TryFrom, sync::Arc, time::Duration}; - -use crate::consensus_orchestrator::RetrieveAuthoritiesFromOrchestrator; -use crate::{collators as collator_util, OrchestratorAuraWorkerAuxData}; /// Parameters for [`run`]. pub struct Params { diff --git a/client/consensus/src/consensus_orchestrator.rs b/client/consensus/src/consensus_orchestrator.rs index 3e22dc01e..1999317eb 100644 --- a/client/consensus/src/consensus_orchestrator.rs +++ b/client/consensus/src/consensus_orchestrator.rs @@ -21,9 +21,7 @@ //! the ParachainConsensus trait to access the orchestrator-dicated authorities, and further //! it implements the TanssiWorker to TanssiOnSlot trait. This trait is use { - crate::AuthorityId, - crate::Pair, - crate::Slot, + crate::{AuthorityId, Pair, Slot}, sc_consensus_slots::{SimpleSlotWorker, SlotInfo, SlotResult}, sp_consensus::Proposer, sp_runtime::traits::Block as BlockT, diff --git a/client/consensus/src/lib.rs b/client/consensus/src/lib.rs index cd8257b32..ead2e712e 100644 --- a/client/consensus/src/lib.rs +++ b/client/consensus/src/lib.rs @@ -20,18 +20,15 @@ //! slot_author returns the author based on the slot number and authorities provided (aura-like) //! authorities retrieves the current set of authorities based on the first eligible key found in the keystore -use {sp_consensus_slots::Slot, sp_core::crypto::Pair}; - pub mod collators; mod consensus_orchestrator; mod manual_seal; + #[cfg(test)] mod tests; -pub use crate::consensus_orchestrator::OrchestratorAuraWorkerAuxData; -pub use sc_consensus_aura::CompatibilityMode; - pub use { + crate::consensus_orchestrator::OrchestratorAuraWorkerAuxData, cumulus_primitives_core::ParaId, manual_seal::{ get_aura_id_from_seed, ContainerManualSealAuraConsensusDataProvider, @@ -39,8 +36,10 @@ pub use { }, pallet_registrar_runtime_api::OnDemandBlockProductionApi, parity_scale_codec::{Decode, Encode}, - sc_consensus_aura::find_pre_digest, - sc_consensus_aura::{slot_duration, AuraVerifier, BuildAuraWorkerParams, SlotProportion}, + sc_consensus_aura::{ + find_pre_digest, slot_duration, AuraVerifier, BuildAuraWorkerParams, CompatibilityMode, + SlotProportion, + }, sc_consensus_slots::InherentDataProviderExt, sp_api::{Core, ProvideRuntimeApi}, sp_application_crypto::AppPublic, @@ -51,6 +50,9 @@ pub use { std::hash::Hash, tp_consensus::TanssiAuthorityAssignmentApi, }; + +use {sp_consensus_slots::Slot, sp_core::crypto::Pair}; + const LOG_TARGET: &str = "aura::tanssi"; type AuthorityId

=

::Public; diff --git a/pallets/collator-assignment/src/assignment.rs b/pallets/collator-assignment/src/assignment.rs index fb17885b6..2c8734cf5 100644 --- a/pallets/collator-assignment/src/assignment.rs +++ b/pallets/collator-assignment/src/assignment.rs @@ -20,12 +20,16 @@ use { cmp, collections::{btree_map::BTreeMap, btree_set::BTreeSet}, marker::PhantomData, - mem, vec, + mem, vec::Vec, }, tp_traits::{ParaId, RemoveInvulnerables as RemoveInvulnerablesT}, }; +// Separate import of `sp_std::vec!` macro, which cause issues with rustfmt if grouped +// with `sp_std::vec::Vec`. +use sp_std::vec; + /// Helper methods to implement collator assignment algorithm pub struct Assignment(PhantomData); diff --git a/pallets/registrar/src/lib.rs b/pallets/registrar/src/lib.rs index 9bc52c405..52b07b0d6 100644 --- a/pallets/registrar/src/lib.rs +++ b/pallets/registrar/src/lib.rs @@ -59,8 +59,7 @@ use { #[frame_support::pallet] pub mod pallet { - use super::*; - use tp_traits::SessionContainerChains; + use {super::*, tp_traits::SessionContainerChains}; #[pallet::pallet] #[pallet::without_storage_info] diff --git a/pallets/stream-payment/Cargo.toml b/pallets/stream-payment/Cargo.toml new file mode 100644 index 000000000..8ea4e83ad --- /dev/null +++ b/pallets/stream-payment/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "pallet-stream-payment" +authors = { workspace = true } +description = "Stream payment pallet" +edition = "2021" +license = "GPL-3.0-only" +version = "0.1.0" + +[package.metadata.docs.rs] +targets = [ "x86_64-unknown-linux-gnu" ] + +[dependencies] +log = { workspace = true } +serde = { workspace = true, optional = true } + +dp-core = { workspace = true } +tp-maths = { workspace = true } +tp-traits = { workspace = true } + +# Substrate +frame-benchmarking = { workspace = true, optional = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +[dev-dependencies] +num-traits = { workspace = true } +pallet-balances = { workspace = true, features = [ "std" ] } +similar-asserts = { workspace = true } +sp-io = { workspace = true, features = [ "std" ] } +tap = { workspace = true } + +[features] +default = [ "std" ] +std = [ + "dp-core/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances/std", + "parity-scale-codec/std", + "scale-info/std", + "serde", + "serde?/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "tp-maths/std", + "tp-traits/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "tp-maths/runtime-benchmarks", + "tp-traits/runtime-benchmarks", +] diff --git a/pallets/stream-payment/README.md b/pallets/stream-payment/README.md new file mode 100644 index 000000000..c85fd904f --- /dev/null +++ b/pallets/stream-payment/README.md @@ -0,0 +1,57 @@ +# Stream payment pallet + +A pallet to create payment streams, where users can setup recurrent payment at some rate per unit of +time. The pallet aims to be configurable and usage agnostic: + +- Runtime configures which assets are supported by providing an `AssetId` type and a type + implementing the `Assets` trait which only requires function needed by the pallet (increase + deposit when creating or refilling a stream, decrease deposit when closing a stream, and + transferring a deposit when the stream payment is performed). Both types allows to easily add new + supported assets in the future while being retro-compatible. The pallet make few assumptions about + how the funds are deposited (thanks to the custom trait), which should allow to easily support + assets from various pallets/sources. +- Runtime configure which unit of time is supported to express the rate of payment. Units of time + should be monotonically increasing. Users can then choose which unit of time they want to use. + +The pallet provides the following calls: +- `open_stream(target, time_unit, asset_id, rate, initial_deposit)`: The origin creates a stream + towards a target (payee), with given time unit, asset and rate. A deposit is made, which is able + to pay for `initial_deposit / rate`. Streams are indexed using a `StreamId` which is returned with + an event. +- `perform_payment(stream_id)`: can be called by anyone to update a stream, performing the payment + for the elapsed time since the last update. All other calls implicitly call `perform_payment`, + such that at any point in time you're guaranteed you'll be able to redeem the payment for the + elapsed time; which allow to call it only when the funds are needed without fear of non-payment. +- `close_stream(stream_id)`: only callable by the source or target of the stream. It pays for the + elapsed time then refund the remaining deposit to the source. +- `immediately_change_deposit(stream_id, asset_id, change)`: Change the deposit in the stream. It + first perform a payment before applying the change, which means a source will not retro-actively + pay for a drained stream. A target that provides services in exchange for payment should suspend + the service as soon as updating the stream would make it drain, and should resume services once + the stream is refilled. The call takes an asset id which must match the config asset id, which + prevents unwanted amounts when a change request that changes the asset is accepted. +- `request_change(stream_id, kind, new_config, deposit_change)`: Allows to request changing the + config of the stream. `kind` states if the change is a mere suggestion or is mandatory, in which + case there is a provided deadline at which point payments will no longer occur. Requests that + don't change the time unit or asset id and change the rate at a disadvantage for the caller is + applied immediately. An existing request can be overritten by both parties if it was a suggestion, + while only by the previous requester if it was mandatory. A nonce is increased to prevent to + prevent one to frontrunner the acceptation of a request with another request. The target of the + stream cannot provide a deposit change, while the source can. It is however mandatory to provide + change with absolute value when changing asset. +- `accept_requested_change(stream_id, request_nonce, deposit_change)`: Accept the change for this + stream id and request nonce. If one want to refuse a change they can either leave it as is (which + will do nothing if the request is a suggestion, or stop payment when reaching the deadline if + mandatory) or close the stream with `close_stream`. The target of the stream cannot provide a + deposit change, while the source can. It is however mandatory to provide change with absolute + value when changing asset. +- `cancel_change_request(stream_id)`: Cancel a change request, only callable by the requester of a + previous request. + +For UIs the pallet provides the following storages: +- `Streams: StreamId => Stream`: stream data indexed by stream id. +- `LookupStreamsWithSource: AccountId => StreamId => ()`: allows to list allow the streams with a + given source by iterating over all storage keys with the key prefix corresponding to the account. +- `LookupStreamsWithTarget: AccountId => StreamId => ()`: same but for the target. Those last 2 + storages are solely for UIs to list incoming and outgoing streams. Key prefix is used to reduce + the POV cost that would require a single Vec of StreamId. \ No newline at end of file diff --git a/pallets/stream-payment/src/benchmarking.rs b/pallets/stream-payment/src/benchmarking.rs new file mode 100644 index 000000000..9d86be2b2 --- /dev/null +++ b/pallets/stream-payment/src/benchmarking.rs @@ -0,0 +1,437 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +use { + crate::{ + Assets, Call, ChangeKind, Config, DepositChange, Event, Pallet, Party, StreamConfig, + Streams, TimeProvider, + }, + frame_benchmarking::{account, impl_benchmark_test_suite, v2::*, BenchmarkError}, + frame_support::{assert_ok, dispatch::RawOrigin}, + frame_system::EventRecord, + sp_std::vec, +}; + +/// Create a funded user. +fn create_funded_user( + string: &'static str, + n: u32, + asset_id: &T::AssetId, + // amount: T::Balance, +) -> T::AccountId { + const SEED: u32 = 0; + let user = account(string, n, SEED); + + // create a large amount that should be greater than ED + let amount: T::Balance = 1_000_000_000u32.into(); + let amount: T::Balance = amount * T::Balance::from(1_000_000_000u32); + T::Assets::bench_set_balance(asset_id, &user, amount); + user +} + +fn assert_last_event(generic_event: ::RuntimeEvent) { + let events = frame_system::Pallet::::events(); + let system_event: ::RuntimeEvent = generic_event.into(); + // compare to the last event record + let EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); +} + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn open_stream() -> Result<(), BenchmarkError> { + let asset_id = T::Assets::bench_worst_case_asset_id(); + let time_unit = T::TimeProvider::bench_worst_case_time_unit(); + + let source = create_funded_user::("source", 1, &asset_id); + let target = create_funded_user::("target", 2, &asset_id); + + #[extrinsic_call] + _( + RawOrigin::Signed(source.clone()), + target, + StreamConfig { + time_unit, + asset_id, + rate: 100u32.into(), + }, + 1_000_000u32.into(), + ); + + assert_last_event::( + Event::StreamOpened { + stream_id: 0u32.into(), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn close_stream() -> Result<(), BenchmarkError> { + // Worst case is closing a stream with a pending payment. + let time_unit = T::TimeProvider::bench_worst_case_time_unit(); + let asset_id = T::Assets::bench_worst_case_asset_id(); + + let source = create_funded_user::("source", 1, &asset_id); + let target = create_funded_user::("target", 2, &asset_id); + + let rate = 100u32.into(); + let initial_deposit = 1_000_000u32.into(); + + assert_ok!(Pallet::::open_stream( + RawOrigin::Signed(source.clone()).into(), + target, + StreamConfig { + time_unit: time_unit.clone(), + asset_id, + rate, + }, + initial_deposit, + )); + + // Change time to trigger payment. + let now = T::TimeProvider::now(&time_unit).expect("can fetch time"); + let delta: T::Balance = 10u32.into(); + T::TimeProvider::bench_set_now(now + delta); + + #[extrinsic_call] + _(RawOrigin::Signed(source.clone()), 0u32.into()); + + assert_last_event::( + Event::StreamClosed { + stream_id: 0u32.into(), + refunded: initial_deposit - (rate * delta), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn perform_payment() -> Result<(), BenchmarkError> { + let time_unit = T::TimeProvider::bench_worst_case_time_unit(); + let asset_id = T::Assets::bench_worst_case_asset_id(); + + let source = create_funded_user::("source", 1, &asset_id); + let target = create_funded_user::("target", 2, &asset_id); + + let rate = 100u32.into(); + let initial_deposit = 1_000_000u32.into(); + + assert_ok!(Pallet::::open_stream( + RawOrigin::Signed(source.clone()).into(), + target.clone(), + StreamConfig { + time_unit: time_unit.clone(), + asset_id, + rate, + }, + initial_deposit, + )); + + // Change time to trigger payment. + let now = T::TimeProvider::now(&time_unit).expect("can fetch time"); + let delta: T::Balance = 10u32.into(); + T::TimeProvider::bench_set_now(now + delta); + + #[extrinsic_call] + _(RawOrigin::Signed(source.clone()), 0u32.into()); + + assert_last_event::( + Event::StreamPayment { + stream_id: 0u32.into(), + source, + target, + amount: rate * delta, + drained: false, + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn request_change_immediate() -> Result<(), BenchmarkError> { + let time_unit = T::TimeProvider::bench_worst_case_time_unit(); + let asset_id = T::Assets::bench_worst_case_asset_id(); + + let source = create_funded_user::("source", 1, &asset_id); + let target = create_funded_user::("target", 2, &asset_id); + + let rate = 100u32.into(); + let initial_deposit = 1_000_000u32.into(); + let config = StreamConfig { + time_unit: time_unit.clone(), + asset_id, + rate, + }; + + assert_ok!(Pallet::::open_stream( + RawOrigin::Signed(source.clone()).into(), + target, + config.clone(), + initial_deposit, + )); + + let new_config = StreamConfig { + rate: 101u32.into(), + ..config.clone() + }; + + #[extrinsic_call] + Pallet::::request_change( + RawOrigin::Signed(source.clone()), + 0u32.into(), + ChangeKind::Suggestion, + new_config.clone(), + Some(DepositChange::Increase(1_000u32.into())), + ); + + assert_last_event::( + Event::StreamConfigChanged { + stream_id: 0u32.into(), + old_config: config, + new_config: new_config, + deposit_change: Some(DepositChange::Increase(1_000u32.into())), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn request_change_delayed() -> Result<(), BenchmarkError> { + let time_unit = T::TimeProvider::bench_worst_case_time_unit(); + let asset_id = T::Assets::bench_worst_case_asset_id(); + let asset_id2 = T::Assets::bench_worst_case_asset_id2(); + + let source = create_funded_user::("source", 1, &asset_id); + let target = create_funded_user::("target", 2, &asset_id); + + let rate = 100u32.into(); + let initial_deposit = 1_000_000u32.into(); + let config = StreamConfig { + time_unit: time_unit.clone(), + asset_id, + rate, + }; + + assert_ok!(Pallet::::open_stream( + RawOrigin::Signed(source.clone()).into(), + target, + config.clone(), + initial_deposit, + )); + + // Change the asset id. In the case asset_id == asset_id2, we decrease the rate so that + // the request is not executed immediately. + let new_config = StreamConfig { + asset_id: asset_id2, + rate: 99u32.into(), + ..config.clone() + }; + + let stream_id = 0u32.into(); + + #[extrinsic_call] + Pallet::::request_change( + RawOrigin::Signed(source.clone()), + stream_id, + ChangeKind::Suggestion, + new_config.clone(), + Some(DepositChange::Absolute(500u32.into())), + ); + + assert_last_event::( + Event::StreamConfigChangeRequested { + stream_id, + request_nonce: 1, + requester: Party::Source, + old_config: config, + new_config, + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn accept_requested_change() -> Result<(), BenchmarkError> { + let time_unit = T::TimeProvider::bench_worst_case_time_unit(); + let asset_id = T::Assets::bench_worst_case_asset_id(); + let asset_id2 = T::Assets::bench_worst_case_asset_id2(); + + let source = create_funded_user::("source", 1, &asset_id); + let target = create_funded_user::("target", 2, &asset_id); + + let rate = 100u32.into(); + let initial_deposit = 1_000_000u32.into(); + let config = StreamConfig { + time_unit: time_unit.clone(), + asset_id, + rate, + }; + + assert_ok!(Pallet::::open_stream( + RawOrigin::Signed(source.clone()).into(), + target.clone(), + config.clone(), + initial_deposit, + )); + + // Change the asset id. In the case asset_id == asset_id2, we decrease the rate so that + // the request is not executed immediately. + let new_config = StreamConfig { + asset_id: asset_id2, + rate: 99u32.into(), + ..config.clone() + }; + + assert_ok!(Pallet::::request_change( + RawOrigin::Signed(source.clone()).into(), + 0u32.into(), + ChangeKind::Suggestion, + new_config.clone(), + Some(DepositChange::Absolute(500u32.into())), + )); + + #[extrinsic_call] + _(RawOrigin::Signed(target.clone()), 0u32.into(), 1, None); + + assert_last_event::( + Event::StreamConfigChanged { + stream_id: 0u32.into(), + old_config: config, + new_config, + deposit_change: Some(DepositChange::Absolute(500u32.into())), + } + .into(), + ); + + Ok(()) + } + + #[benchmark] + fn cancel_change_request() -> Result<(), BenchmarkError> { + let time_unit = T::TimeProvider::bench_worst_case_time_unit(); + let asset_id = T::Assets::bench_worst_case_asset_id(); + let asset_id2 = T::Assets::bench_worst_case_asset_id2(); + + let source = create_funded_user::("source", 1, &asset_id); + let target = create_funded_user::("target", 2, &asset_id); + + let rate = 100u32.into(); + let initial_deposit = 1_000_000u32.into(); + let config = StreamConfig { + time_unit: time_unit.clone(), + asset_id, + rate, + }; + + assert_ok!(Pallet::::open_stream( + RawOrigin::Signed(source.clone()).into(), + target.clone(), + config.clone(), + initial_deposit, + )); + + // Change the asset id. In the case asset_id == asset_id2, we decrease the rate so that + // the request is not executed immediately. + let new_config = StreamConfig { + asset_id: asset_id2, + rate: 99u32.into(), + ..config.clone() + }; + + assert_ok!(Pallet::::request_change( + RawOrigin::Signed(source.clone()).into(), + 0u32.into(), + ChangeKind::Suggestion, + new_config.clone(), + Some(DepositChange::Absolute(500u32.into())), + )); + + #[extrinsic_call] + _(RawOrigin::Signed(source), 0u32.into()); + + let stream_id: T::StreamId = 0u32.into(); + assert!(Streams::::get(stream_id) + .expect("to be a stream") + .pending_request + .is_none()); + + Ok(()) + } + + #[benchmark] + fn immediately_change_deposit() -> Result<(), BenchmarkError> { + let time_unit = T::TimeProvider::bench_worst_case_time_unit(); + let asset_id = T::Assets::bench_worst_case_asset_id(); + + let source = create_funded_user::("source", 1, &asset_id); + let target = create_funded_user::("target", 2, &asset_id); + + let rate = 100u32.into(); + let initial_deposit = 1_000_000u32.into(); + let config = StreamConfig { + time_unit: time_unit.clone(), + asset_id: asset_id.clone(), + rate, + }; + + assert_ok!(Pallet::::open_stream( + RawOrigin::Signed(source.clone()).into(), + target.clone(), + config.clone(), + initial_deposit, + )); + + #[extrinsic_call] + _( + RawOrigin::Signed(source), + 0u32.into(), + asset_id, + DepositChange::Absolute(500u32.into()), + ); + + assert_last_event::( + Event::StreamConfigChanged { + stream_id: 0u32.into(), + old_config: config.clone(), + new_config: config, + deposit_change: Some(DepositChange::Absolute(500u32.into())), + } + .into(), + ); + + Ok(()) + } + + impl_benchmark_test_suite!( + Pallet, + crate::mock::ExtBuilder::default().build(), + crate::mock::Runtime, + ); +} diff --git a/pallets/stream-payment/src/lib.rs b/pallets/stream-payment/src/lib.rs new file mode 100644 index 000000000..9a6f01530 --- /dev/null +++ b/pallets/stream-payment/src/lib.rs @@ -0,0 +1,938 @@ +// Copyright (C) Moondance Labs Ltd. +// This file is part of Tanssi. + +// Tanssi is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Tanssi is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Tanssi. If not, see + +#![doc = include_str!("../README.md")] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +pub mod weights; + +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; + +use { + crate::weights::WeightInfo, + core::cmp::min, + frame_support::{ + dispatch::DispatchErrorWithPostInfo, + pallet, + pallet_prelude::*, + storage::types::{StorageDoubleMap, StorageMap}, + traits::tokens::Balance, + Blake2_128Concat, + }, + frame_system::pallet_prelude::*, + parity_scale_codec::{FullCodec, MaxEncodedLen}, + scale_info::TypeInfo, + sp_runtime::{ + traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, One, Saturating, Zero}, + ArithmeticError, + }, + sp_std::{fmt::Debug, marker::PhantomData}, +}; + +pub use pallet::*; + +/// Type able to provide the current time for given unit. +/// For each unit the returned number should monotonically increase and not +/// overflow. +pub trait TimeProvider { + fn now(unit: &Unit) -> Option; + + /// Benchmarks: should return the time unit which has the worst performance calling + /// `TimeProvider::now(unit)` with. + #[cfg(feature = "runtime-benchmarks")] + fn bench_worst_case_time_unit() -> Unit; + + /// Benchmarks: sets the "now" time for time unit returned by `bench_worst_case_time_unit`. + #[cfg(feature = "runtime-benchmarks")] + fn bench_set_now(instant: Number); +} + +/// Interactions the pallet needs with assets. +pub trait Assets { + /// Transfer assets deposited by an account to another account. + /// Those assets should not be considered deposited in the target account. + fn transfer_deposit( + asset_id: &AssetId, + from: &AccountId, + to: &AccountId, + amount: Balance, + ) -> DispatchResult; + + /// Increase the deposit for an account and asset id. Should fail if account doesn't have + /// enough of that asset. Funds should be safe and not slashable. + fn increase_deposit(asset_id: &AssetId, account: &AccountId, amount: Balance) + -> DispatchResult; + + /// Decrease the deposit for an account and asset id. Should fail on underflow. + fn decrease_deposit(asset_id: &AssetId, account: &AccountId, amount: Balance) + -> DispatchResult; + + /// Return the deposit for given asset and account. + fn get_deposit(asset_id: &AssetId, account: &AccountId) -> Balance; + + /// Benchmarks: should return the asset id which has the worst performance when interacting + /// with it. + #[cfg(feature = "runtime-benchmarks")] + fn bench_worst_case_asset_id() -> AssetId; + + /// Benchmarks: should return the another asset id which has the worst performance when interacting + /// with it afther `bench_worst_case_asset_id`. This is to benchmark the worst case when changing config + /// from one asset to another. If there is only one asset id it is fine to return it in both + /// `bench_worst_case_asset_id` and `bench_worst_case_asset_id2`. + #[cfg(feature = "runtime-benchmarks")] + fn bench_worst_case_asset_id2() -> AssetId; + + /// Benchmarks: should set the balance. + #[cfg(feature = "runtime-benchmarks")] + fn bench_set_balance(asset_id: &AssetId, account: &AccountId, amount: Balance); +} + +#[pallet] +pub mod pallet { + use super::*; + + /// Pooled Staking pallet. + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// Overarching event type + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Type used to represent stream ids. Should be large enough to not overflow. + type StreamId: AtLeast32BitUnsigned + + Default + + Debug + + Copy + + Clone + + FullCodec + + TypeInfo + + MaxEncodedLen; + + /// The balance type, which is also the type representing time (as this + /// pallet will do math with both time and balances to compute how + /// much should be paid). + type Balance: Balance; + + /// Type representing an asset id, a identifier allowing distinguishing assets. + type AssetId: Debug + Clone + FullCodec + TypeInfo + MaxEncodedLen + PartialEq + Eq; + + /// Provide interaction with assets. + type Assets: Assets; + + /// Represents which units of time can be used. Designed to be an enum + /// with a variant for each kind of time source/scale supported. + type TimeUnit: Debug + Clone + FullCodec + TypeInfo + MaxEncodedLen + Eq; + + /// Provide the current time in given unit. + type TimeProvider: TimeProvider; + + type WeightInfo: weights::WeightInfo; + } + + type AccountIdOf = ::AccountId; + type AssetIdOf = ::AssetId; + + pub type RequestNonce = u32; + + /// A stream payment from source to target. + /// Stores the last time the stream was updated, which allows to compute + /// elapsed time and perform payment. + #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] + #[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Clone, TypeInfo)] + pub struct Stream { + /// Payer, source of the stream. + pub source: AccountId, + /// Payee, target of the stream. + pub target: AccountId, + /// Steam config (time unit, asset id, rate) + pub config: StreamConfig, + /// How much is deposited to fund this stream. + pub deposit: Balance, + /// Last time the stream was updated in `config.time_unit`. + pub last_time_updated: Balance, + /// Nonce for requests. This prevents a request to make a first request + /// then change it to another request to frontrun the other party + /// accepting. + pub request_nonce: RequestNonce, + /// A pending change request if any. + pub pending_request: Option>, + } + + impl Stream { + pub fn account_to_party(&self, account: AccountId) -> Option { + match account { + a if a == self.source => Some(Party::Source), + a if a == self.target => Some(Party::Target), + _ => None, + } + } + } + + /// Stream configuration. + #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] + #[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Copy, Clone, TypeInfo)] + pub struct StreamConfig { + /// Unit in which time is measured using a `TimeProvider`. + pub time_unit: Unit, + /// Asset used for payment. + pub asset_id: AssetId, + /// Amount of asset / unit. + pub rate: Balance, + } + + /// Origin of a change request. + #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] + #[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Copy, Clone, TypeInfo)] + pub enum Party { + Source, + Target, + } + + impl Party { + pub fn inverse(self) -> Self { + match self { + Party::Source => Party::Target, + Party::Target => Party::Source, + } + } + } + + /// Kind of change requested. + #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] + #[derive(RuntimeDebug, PartialEq, Eq, Encode, Decode, Copy, Clone, TypeInfo)] + pub enum ChangeKind