diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index de33f6541..47211e8e0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -130,7 +130,7 @@ jobs: run: rustup show - name: Install zepter - run: cargo install --locked -f zepter + run: cargo install --locked -f zepter --version 1.1.0 - name: Run zepter run: zepter run check diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml new file mode 100644 index 000000000..874eaabdf --- /dev/null +++ b/.github/workflows/version-bump.yml @@ -0,0 +1,35 @@ +name: Create version bump ticket +on: + workflow_dispatch: + inputs: + from: + description: "Polkadot version to bump from (ex: v0.9.40)" + required: true + to: + description: "Polkadot version to bump to (ex: v0.9.42)" + required: true + +jobs: + create_bump_ticket: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 20.10.0 + - name: Generate version bump issue + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + cd tools + yarn install + yarn --silent run print-version-bump-info -- --from ${{ github.event.inputs.from }} --to ${{ github.event.inputs.to }} | tee ../version-bump.md + - name: Create version bump issue + uses: peter-evans/create-issue-from-file@v3 + with: + title: Update polkadot-sdk from ${{ github.event.inputs.from }} to ${{ github.event.inputs.to }} + content-filepath: ./version-bump.md + labels: | + automated issue \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d6c8fdd20..5dadd07bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2856,6 +2856,7 @@ dependencies = [ "pallet-services-payment", "pallet-session", "pallet-staking", + "pallet-stream-payment", "pallet-sudo", "pallet-timestamp", "pallet-transaction-payment", @@ -2872,6 +2873,7 @@ dependencies = [ "polkadot-runtime-common", "polkadot-runtime-parachains", "polkadot-service", + "runtime-common", "sc-consensus-grandpa", "scale-info", "serde", @@ -3989,6 +3991,7 @@ dependencies = [ "pallet-root-testing", "pallet-services-payment", "pallet-session", + "pallet-stream-payment", "pallet-sudo", "pallet-timestamp", "pallet-transaction-payment", @@ -4001,6 +4004,7 @@ dependencies = [ "polkadot-parachain-primitives", "polkadot-runtime-common", "polkadot-runtime-parachains", + "runtime-common", "sc-consensus-grandpa", "scale-info", "serde", @@ -8737,6 +8741,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" @@ -11273,6 +11301,30 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "runtime-common" +version = "0.1.0" +dependencies = [ + "cumulus-primitives-core", + "frame-support", + "frame-system", + "frame-try-runtime", + "hex-literal 0.3.4", + "pallet-balances", + "pallet-configuration", + "pallet-data-preservers", + "pallet-invulnerables", + "pallet-migrations", + "pallet-pooled-staking", + "pallet-registrar", + "pallet-services-payment", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-runtime", + "sp-std", +] + [[package]] name = "rustc-demangle" version = "0.1.23" diff --git a/Cargo.toml b/Cargo.toml index e5403da3d..c7aa2e8b5 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 } @@ -43,10 +44,12 @@ dancebox-runtime = { path = "runtime/dancebox", default-features = false } flashbox-runtime = { path = "runtime/flashbox", default-features = false } manual-xcm-rpc = { path = "client/manual-xcm" } node-common = { path = "client/node-common" } +runtime-common = { path = "runtime/common", default-features = false } 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-impl-tanssi-pallets-config = { path = "primitives/impl-tanssi-pallets-config", default-features = false } tp-maths = { path = "primitives/maths", default-features = false } tp-traits = { path = "primitives/traits", default-features = false } @@ -253,6 +256,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/lib.rs b/client/consensus/src/lib.rs index 770ceda3b..ead2e712e 100644 --- a/client/consensus/src/lib.rs +++ b/client/consensus/src/lib.rs @@ -20,11 +20,10 @@ //! 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; @@ -52,6 +51,8 @@ pub use { tp_consensus::TanssiAuthorityAssignmentApi, }; +use {sp_consensus_slots::Slot, sp_core::crypto::Pair}; + const LOG_TARGET: &str = "aura::tanssi"; type AuthorityId

=

::Public; 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/chain_spec/dancebox.rs b/node/src/chain_spec/dancebox.rs index d5a6c6f28..d10390c10 100644 --- a/node/src/chain_spec/dancebox.rs +++ b/node/src/chain_spec/dancebox.rs @@ -212,9 +212,10 @@ fn testnet_genesis( ) .collect(); // Assign 1000 block credits to all container chains registered in genesis + // Assign 100 collator assignment credits to all container chains registered in genesis let para_id_credits: Vec<_> = para_ids .iter() - .map(|(para_id, _genesis_data, _boot_nodes)| (*para_id, 1000)) + .map(|(para_id, _genesis_data, _boot_nodes)| (*para_id, 1000, 100).into()) .collect(); let para_id_boot_nodes: Vec<_> = para_ids .iter() diff --git a/node/src/chain_spec/flashbox.rs b/node/src/chain_spec/flashbox.rs index 70821c27e..a7573b4fb 100644 --- a/node/src/chain_spec/flashbox.rs +++ b/node/src/chain_spec/flashbox.rs @@ -212,9 +212,10 @@ fn testnet_genesis( ) .collect(); // Assign 1000 block credits to all container chains registered in genesis + // Assign 100 collator assignment credits to all container chains registered in genesis let para_id_credits: Vec<_> = para_ids .iter() - .map(|(para_id, _genesis_data, _boot_nodes)| (*para_id, 1000)) + .map(|(para_id, _genesis_data, _boot_nodes)| (*para_id, 1000, 100).into()) .collect(); let para_id_boot_nodes: Vec<_> = para_ids .iter() 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, diff --git a/pallets/collator-assignment/src/lib.rs b/pallets/collator-assignment/src/lib.rs index 3cbb01d7c..b1dde8077 100644 --- a/pallets/collator-assignment/src/lib.rs +++ b/pallets/collator-assignment/src/lib.rs @@ -58,8 +58,9 @@ use { }, sp_std::{fmt::Debug, prelude::*, vec}, tp_traits::{ - GetContainerChainAuthor, GetHostConfiguration, GetSessionContainerChains, ParaId, - RemoveInvulnerables, RemoveParaIdsWithNoCredits, ShouldRotateAllCollators, Slot, + CollatorAssignmentHook, GetContainerChainAuthor, GetHostConfiguration, + GetSessionContainerChains, ParaId, RemoveInvulnerables, RemoveParaIdsWithNoCredits, + ShouldRotateAllCollators, Slot, }, }; @@ -102,6 +103,7 @@ pub mod pallet { type GetRandomnessForNextBlock: GetRandomnessForNextBlock>; type RemoveInvulnerables: RemoveInvulnerables; type RemoveParaIdsWithNoCredits: RemoveParaIdsWithNoCredits; + type CollatorAssignmentHook: CollatorAssignmentHook; /// The weight information of this pallet. type WeightInfo: WeightInfo; } @@ -285,6 +287,30 @@ pub mod pallet { } }; + // TODO: this probably is asking for a refactor + // only apply the onCollatorAssignedHook if sufficient collators + for para_id in &container_chain_ids { + if !new_assigned + .container_chains + .get(para_id) + .unwrap_or(&vec![]) + .is_empty() + { + T::CollatorAssignmentHook::on_collators_assigned(*para_id); + } + } + + for para_id in ¶threads { + if !new_assigned + .container_chains + .get(para_id) + .unwrap_or(&vec![]) + .is_empty() + { + T::CollatorAssignmentHook::on_collators_assigned(*para_id); + } + } + let mut pending = PendingCollatorContainerChain::::get(); let old_assigned_changed = old_assigned != new_assigned; let mut pending_changed = false; diff --git a/pallets/collator-assignment/src/mock.rs b/pallets/collator-assignment/src/mock.rs index b3841e744..4c9740e16 100644 --- a/pallets/collator-assignment/src/mock.rs +++ b/pallets/collator-assignment/src/mock.rs @@ -235,6 +235,7 @@ impl pallet_collator_assignment::Config for Test { type GetRandomnessForNextBlock = MockGetRandomnessForNextBlock; type RemoveInvulnerables = RemoveAccountIdsAbove100; type RemoveParaIdsWithNoCredits = RemoveParaIdsAbove5000; + type CollatorAssignmentHook = (); type WeightInfo = (); } diff --git a/pallets/services-payment/Cargo.toml b/pallets/services-payment/Cargo.toml index d90a5175b..099768739 100644 --- a/pallets/services-payment/Cargo.toml +++ b/pallets/services-payment/Cargo.toml @@ -16,7 +16,7 @@ frame-system = { workspace = true } log = { workspace = true } parity-scale-codec = { workspace = true, features = [ "derive", "max-encoded-len" ] } scale-info = { workspace = true } -serde = { workspace = true, optional = true, features = [ "derive" ] } +serde = { workspace = true, default-features = false, features = [ "derive" ] } sp-io = { workspace = true } sp-runtime = { workspace = true } sp-std = { workspace = true } @@ -38,7 +38,7 @@ std = [ "pallet-balances/std", "parity-scale-codec/std", "scale-info/std", - "serde?/std", + "serde/std", "sp-core/std", "sp-io/std", "sp-runtime/std", diff --git a/pallets/services-payment/src/benchmarks.rs b/pallets/services-payment/src/benchmarks.rs index 5655a4f10..ca767687a 100644 --- a/pallets/services-payment/src/benchmarks.rs +++ b/pallets/services-payment/src/benchmarks.rs @@ -18,16 +18,19 @@ //! Benchmarking use { - crate::{BalanceOf, BlockNumberFor, Call, Config, Pallet, ProvideBlockProductionCost}, + crate::{ + BalanceOf, BlockNumberFor, Call, Config, Pallet, ProvideBlockProductionCost, + ProvideCollatorAssignmentCost, + }, frame_benchmarking::{account, v2::*}, frame_support::{ assert_ok, - traits::{Currency, Get}, + traits::{Currency, EnsureOriginWithArg, Get}, }, frame_system::RawOrigin, sp_runtime::Saturating, sp_std::prelude::*, - tp_traits::AuthorNotingHook, + tp_traits::{AuthorNotingHook, CollatorAssignmentHook}, }; // Build genesis storage according to the mock runtime. @@ -82,11 +85,11 @@ mod benchmarks { } #[benchmark] - fn set_credits() { + fn set_block_production_credits() { let para_id = 1001u32.into(); - let credits = T::MaxCreditsStored::get(); + let credits = T::FreeBlockProductionCredits::get(); - assert_ok!(Pallet::::set_credits( + assert_ok!(Pallet::::set_block_production_credits( RawOrigin::Root.into(), para_id, credits, @@ -95,11 +98,11 @@ mod benchmarks { // Before call: 1000 credits assert_eq!( crate::BlockProductionCredits::::get(¶_id).unwrap_or_default(), - T::MaxCreditsStored::get() + T::FreeBlockProductionCredits::get() ); #[extrinsic_call] - Pallet::::set_credits(RawOrigin::Root, para_id, 1u32.into()); + Pallet::::set_block_production_credits(RawOrigin::Root, para_id, 1u32.into()); // After call: 1 credit assert_eq!( @@ -122,11 +125,30 @@ 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; let block_cost = T::ProvideBlockProductionCost::block_cost(¶_id.into()).0; - let max_credit_stored = T::MaxCreditsStored::get(); + let max_credit_stored = T::FreeBlockProductionCredits::get(); let balance_to_purchase = block_cost.saturating_mul(max_credit_stored.into()); let caller = create_funded_user::("caller", 1, 1_000_000_000u32); let existential_deposit = ::minimum_balance(); @@ -145,5 +167,25 @@ mod benchmarks { } } + #[benchmark] + fn on_collators_assigned() { + let para_id = 1001u32; + let collator_assignment_cost = + T::ProvideCollatorAssignmentCost::collator_assignment_cost(¶_id.into()).0; + let max_credit_stored = T::FreeCollatorAssignmentCredits::get(); + let balance_to_purchase = collator_assignment_cost.saturating_mul(max_credit_stored.into()); + let caller = create_funded_user::("caller", 1, 1_000_000_000u32); + let existential_deposit = ::minimum_balance(); + assert_ok!(Pallet::::purchase_credits( + RawOrigin::Signed(caller.clone()).into(), + para_id.into(), + balance_to_purchase + existential_deposit + )); + #[block] + { + as CollatorAssignmentHook>::on_collators_assigned(para_id.into()); + } + } + impl_benchmark_test_suite!(Pallet, crate::benchmarks::new_test_ext(), crate::mock::Test); } diff --git a/pallets/services-payment/src/lib.rs b/pallets/services-payment/src/lib.rs index 6fa73ade4..229d917ed 100644 --- a/pallets/services-payment/src/lib.rs +++ b/pallets/services-payment/src/lib.rs @@ -41,13 +41,17 @@ 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, + serde::{Deserialize, Serialize}, sp_io::hashing::blake2_256, sp_runtime::traits::TrailingZeroInput, - tp_traits::{AuthorNotingHook, BlockNumber}, + tp_traits::{AuthorNotingHook, BlockNumber, CollatorAssignmentHook}, }; #[cfg(any(test, feature = "runtime-benchmarks"))] @@ -71,13 +75,24 @@ pub mod pallet { type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// Handler for fees type OnChargeForBlock: OnUnbalanced>; + + /// Handler for fees + type OnChargeForCollatorAssignment: OnUnbalanced>; + /// Currency type for fee payment type Currency: Currency; /// Provider of a block cost which can adjust from block to block type ProvideBlockProductionCost: ProvideBlockProductionCost; + /// Provider of a block cost which can adjust from block to block + type ProvideCollatorAssignmentCost: ProvideCollatorAssignmentCost; + + /// The maximum number of block production credits that can be accumulated + type FreeBlockProductionCredits: Get>; - /// The maximum number of credits that can be accumulated - type MaxCreditsStored: Get>; + /// The maximum number of collator assigment production credits that can be accumulated + type FreeCollatorAssignmentCredits: Get; + // Who can call set_refund_address? + type SetRefundAddressOrigin: EnsureOriginWithArg; type WeightInfo: WeightInfo; } @@ -100,14 +115,26 @@ pub mod pallet { payer: T::AccountId, credit: BalanceOf, }, - CreditBurned { + BlockProductionCreditBurned { para_id: ParaId, credits_remaining: BlockNumberFor, }, - CreditsSet { + CollatorAssignmentCreditBurned { + para_id: ParaId, + credits_remaining: u32, + }, + BlockProductionCreditsSet { para_id: ParaId, credits: BlockNumberFor, }, + RefundAddressUpdated { + para_id: ParaId, + refund_address: Option, + }, + CollatorAssignmentCreditsSet { + para_id: ParaId, + credits: u32, + }, } #[pallet::storage] @@ -115,11 +142,22 @@ pub mod pallet { pub type BlockProductionCredits = StorageMap<_, Blake2_128Concat, ParaId, BlockNumberFor, OptionQuery>; + #[pallet::storage] + #[pallet::getter(fn free_collator_assignment_credits)] + pub type CollatorAssignmentCredits = + StorageMap<_, Blake2_128Concat, ParaId, u32, OptionQuery>; + /// List of para ids that have already been given free credits #[pallet::storage] #[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 @@ -154,20 +192,14 @@ pub mod pallet { /// Can only be called by root. #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::set_credits())] - pub fn set_credits( + pub fn set_block_production_credits( origin: OriginFor, para_id: ParaId, - credits: BlockNumberFor, + free_block_credits: BlockNumberFor, ) -> DispatchResultWithPostInfo { ensure_root(origin)?; - if credits.is_zero() { - BlockProductionCredits::::remove(para_id); - } else { - BlockProductionCredits::::insert(para_id, credits); - } - - Self::deposit_event(Event::::CreditsSet { para_id, credits }); + Self::set_free_block_production_credits(¶_id, free_block_credits); Ok(().into()) } @@ -191,11 +223,53 @@ 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()) + } + + /// Set the number of block production credits for this para_id without paying for them. + /// Can only be called by root. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::set_credits())] + pub fn set_collator_assignment_credits( + origin: OriginFor, + para_id: ParaId, + free_collator_assignment_credits: u32, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + + Self::set_free_collator_assignment_credits(¶_id, free_collator_assignment_credits); + + Ok(().into()) + } } impl Pallet { /// Burn a credit for the given para. Deducts one credit if possible, errors otherwise. - pub fn burn_free_credit_for_para(para_id: &ParaId) -> DispatchResultWithPostInfo { + pub fn burn_block_production_free_credit_for_para( + para_id: &ParaId, + ) -> DispatchResultWithPostInfo { let existing_credits = BlockProductionCredits::::get(para_id).unwrap_or(BlockNumberFor::::zero()); @@ -207,7 +281,26 @@ pub mod pallet { let updated_credits = existing_credits.saturating_sub(1u32.into()); BlockProductionCredits::::insert(para_id, updated_credits); - Self::deposit_event(Event::::CreditBurned { + Self::deposit_event(Event::::BlockProductionCreditBurned { + para_id: *para_id, + credits_remaining: updated_credits, + }); + + Ok(().into()) + } + + /// Burn a credit for the given para. Deducts one credit if possible, errors otherwise. + pub fn burn_collator_assignment_free_credit_for_para( + para_id: &ParaId, + ) -> DispatchResultWithPostInfo { + let existing_credits = CollatorAssignmentCredits::::get(para_id).unwrap_or(0u32); + + ensure!(existing_credits >= 1u32, Error::::InsufficientCredits,); + + let updated_credits = existing_credits.saturating_sub(1u32); + CollatorAssignmentCredits::::insert(para_id, updated_credits); + + Self::deposit_event(Event::::CollatorAssignmentCreditBurned { para_id: *para_id, credits_remaining: updated_credits, }); @@ -220,17 +313,27 @@ pub mod pallet { // This para id has already received free credits return Weight::default(); } - // Set number of credits to MaxCreditsStored - let existing_credits = + + // Set number of credits to FreeBlockProductionCredits + let block_production_existing_credits = BlockProductionCredits::::get(para_id).unwrap_or(BlockNumberFor::::zero()); - let updated_credits = T::MaxCreditsStored::get(); + let block_production_updated_credits = T::FreeBlockProductionCredits::get(); // Do not update credits if for some reason this para id had more - if existing_credits < updated_credits { - BlockProductionCredits::::insert(para_id, updated_credits); - Self::deposit_event(Event::::CreditsSet { - para_id: *para_id, - credits: updated_credits, - }); + if block_production_existing_credits < block_production_updated_credits { + Self::set_free_block_production_credits(¶_id, block_production_updated_credits); + } + + // Set number of credits to FreeCollatorAssignmentCredits + let collator_assignment_existing_credits = + CollatorAssignmentCredits::::get(para_id).unwrap_or(0u32); + let collator_assignment_updated_credits = T::FreeCollatorAssignmentCredits::get(); + + // Do not update credits if for some reason this para id had more + if collator_assignment_existing_credits < collator_assignment_updated_credits { + Self::set_free_collator_assignment_credits( + ¶_id, + collator_assignment_updated_credits, + ); } // We only allow to call this function once per para id, even if it didn't actually @@ -239,11 +342,46 @@ pub mod pallet { Weight::default() } + + pub fn set_free_collator_assignment_credits( + para_id: &ParaId, + free_collator_assignment_credits: u32, + ) { + if free_collator_assignment_credits.is_zero() { + CollatorAssignmentCredits::::remove(para_id); + } else { + CollatorAssignmentCredits::::insert(para_id, free_collator_assignment_credits); + } + + Self::deposit_event(Event::::CollatorAssignmentCreditsSet { + para_id: *para_id, + credits: free_collator_assignment_credits, + }); + } + + pub fn set_free_block_production_credits( + para_id: &ParaId, + free_collator_block_production_credits: BlockNumberFor, + ) { + if free_collator_block_production_credits.is_zero() { + BlockProductionCredits::::remove(para_id); + } else { + BlockProductionCredits::::insert( + para_id, + free_collator_block_production_credits, + ); + } + + Self::deposit_event(Event::::BlockProductionCreditsSet { + para_id: *para_id, + credits: free_collator_block_production_credits, + }); + } } #[pallet::genesis_config] pub struct GenesisConfig { - pub para_id_credits: Vec<(ParaId, BlockNumberFor)>, + pub para_id_credits: Vec>>, } impl Default for GenesisConfig { @@ -257,13 +395,39 @@ pub mod pallet { #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { - for (para_id, credits) in &self.para_id_credits { - BlockProductionCredits::::insert(para_id, credits); + for para_id_credits in &self.para_id_credits { + BlockProductionCredits::::insert( + para_id_credits.para_id, + para_id_credits.block_production_credits, + ); + CollatorAssignmentCredits::::insert( + para_id_credits.para_id, + para_id_credits.collator_assignment_credits, + ); } } } } +// Params to be set in genesis +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, Serialize, Deserialize)] +pub struct FreeCreditGenesisParams { + pub para_id: ParaId, + pub block_production_credits: BlockProductCredits, + pub collator_assignment_credits: u32, +} +impl From<(ParaId, BlockProductCredits, u32)> + for FreeCreditGenesisParams +{ + fn from(value: (ParaId, BlockProductCredits, u32)) -> Self { + Self { + para_id: value.0, + block_production_credits: value.1, + collator_assignment_credits: value.2, + } + } +} + /// Balance used by this pallet pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; @@ -281,6 +445,12 @@ pub trait ProvideBlockProductionCost { fn block_cost(para_id: &ParaId) -> (BalanceOf, Weight); } +/// Returns the cost for a given block credit at the current time. This can be a complex operation, +/// so it also returns the weight it consumes. (TODO: or just rely on benchmarking) +pub trait ProvideCollatorAssignmentCost { + fn collator_assignment_cost(para_id: &ParaId) -> (BalanceOf, Weight); +} + impl AuthorNotingHook for Pallet { // This hook is called when pallet_author_noting sees that the block number of a container chain has increased. // Currently we always charge 1 credit, even if a container chain produced more that 1 block in between tanssi @@ -290,7 +460,7 @@ impl AuthorNotingHook for Pallet { _block_number: BlockNumber, para_id: ParaId, ) -> Weight { - if Pallet::::burn_free_credit_for_para(¶_id).is_err() { + if Pallet::::burn_block_production_free_credit_for_para(¶_id).is_err() { let (amount_to_charge, _weight) = T::ProvideBlockProductionCost::block_cost(¶_id); match T::Currency::withdraw( &Self::parachain_tank(para_id), @@ -299,7 +469,7 @@ impl AuthorNotingHook for Pallet { ExistenceRequirement::KeepAlive, ) { Err(e) => log::warn!( - "Failed to withdraw credits for container chain {}: {:?}", + "Failed to withdraw block production payment for container chain {}: {:?}", u32::from(para_id), e ), @@ -313,6 +483,31 @@ impl AuthorNotingHook for Pallet { } } +impl CollatorAssignmentHook for Pallet { + fn on_collators_assigned(para_id: ParaId) -> Weight { + if Pallet::::burn_collator_assignment_free_credit_for_para(¶_id).is_err() { + let (amount_to_charge, _weight) = + T::ProvideCollatorAssignmentCost::collator_assignment_cost(¶_id); + match T::Currency::withdraw( + &Self::parachain_tank(para_id), + amount_to_charge, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + ) { + Err(e) => log::warn!( + "Failed to withdraw collator assignment payment for container chain {}: {:?}", + u32::from(para_id), + e + ), + Ok(imbalance) => { + T::OnChargeForCollatorAssignment::on_unbalanced(imbalance); + } + } + } + T::WeightInfo::on_collators_assigned() + } +} + impl Pallet { /// Derive a derivative account ID from the paraId. pub fn parachain_tank(para_id: ParaId) -> T::AccountId { @@ -332,12 +527,20 @@ 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); + CollatorAssignmentCredits::::remove(para_id); } } diff --git a/pallets/services-payment/src/mock.rs b/pallets/services-payment/src/mock.rs index 07a15aec8..caa7b30bc 100644 --- a/pallets/services-payment/src/mock.rs +++ b/pallets/services-payment/src/mock.rs @@ -29,13 +29,16 @@ //! to that containerChain, by simply assigning the slot position. use { - crate::{self as pallet_services_payment, ProvideBlockProductionCost}, + crate::{ + self as pallet_services_payment, ProvideBlockProductionCost, ProvideCollatorAssignmentCost, + }, cumulus_primitives_core::ParaId, frame_support::{ pallet_prelude::*, parameter_types, traits::{ConstU32, ConstU64, Everything}, }, + frame_system::EnsureRoot, sp_core::H256, sp_runtime::{ traits::{BlakeTwo256, IdentityLookup}, @@ -104,19 +107,25 @@ impl pallet_balances::Config for Test { } parameter_types! { - pub const MaxCreditsStored: u64 = 5; + pub const FreeBlockProductionCredits: u64 = 5; + pub const FreeCollatorAssignmentCredits: u32 = 5; } impl pallet_services_payment::Config for Test { type RuntimeEvent = RuntimeEvent; type OnChargeForBlock = (); + type OnChargeForCollatorAssignment = (); type Currency = Balances; type ProvideBlockProductionCost = BlockProductionCost; - type MaxCreditsStored = MaxCreditsStored; + type ProvideCollatorAssignmentCost = CollatorAssignmentProductionCost; + type FreeBlockProductionCredits = FreeBlockProductionCredits; + type FreeCollatorAssignmentCredits = FreeCollatorAssignmentCredits; + type SetRefundAddressOrigin = EnsureRoot; type WeightInfo = (); } pub(crate) const FIXED_BLOCK_PRODUCTION_COST: u128 = 100; +pub(crate) const FIXED_COLLATOR_ASSIGNMENT_COST: u128 = 200; pub struct BlockProductionCost(PhantomData); impl ProvideBlockProductionCost for BlockProductionCost { @@ -125,6 +134,13 @@ impl ProvideBlockProductionCost for BlockProductionCost { } } +pub struct CollatorAssignmentProductionCost(PhantomData); +impl ProvideCollatorAssignmentCost for CollatorAssignmentProductionCost { + fn collator_assignment_cost(_para_id: &ParaId) -> (u128, Weight) { + (FIXED_COLLATOR_ASSIGNMENT_COST, Weight::zero()) + } +} + #[derive(Default)] pub struct ExtBuilder { balances: Vec<(AccountId, Balance)>, diff --git a/pallets/services-payment/src/tests.rs b/pallets/services-payment/src/tests.rs index 0b2ec9e41..66d2dfa25 100644 --- a/pallets/services-payment/src/tests.rs +++ b/pallets/services-payment/src/tests.rs @@ -29,11 +29,14 @@ //! 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, + CollatorAssignmentCredits, RefundAddress, + }, cumulus_primitives_core::ParaId, frame_support::{assert_err, assert_ok, traits::fungible::Inspect}, sp_runtime::DispatchError, - tp_traits::AuthorNotingHook, + tp_traits::{AuthorNotingHook, CollatorAssignmentHook}, }; const ALICE: u64 = 1; @@ -84,20 +87,24 @@ fn purchase_credits_fails_with_insufficient_balance() { fn burn_credit_fails_with_no_credits() { ExtBuilder::default().build().execute_with(|| { assert_err!( - PaymentServices::burn_free_credit_for_para(&1u32.into()), + PaymentServices::burn_block_production_free_credit_for_para(&1u32.into()), + pallet_services_payment::Error::::InsufficientCredits, + ); + assert_err!( + PaymentServices::burn_collator_assignment_free_credit_for_para(&1u32.into()), pallet_services_payment::Error::::InsufficientCredits, ); }); } #[test] -fn burn_credit_works() { +fn burn_block_production_credit_works() { ExtBuilder::default() .with_balances([(ALICE, 1_000)].into()) .build() .execute_with(|| { let para_id = 1.into(); - assert_ok!(PaymentServices::set_credits( + assert_ok!(PaymentServices::set_block_production_credits( RuntimeOrigin::root(), para_id, 1u64, @@ -105,12 +112,40 @@ fn burn_credit_works() { // should succeed and burn one assert_eq!(>::get(para_id), Some(1u64)); - assert_ok!(PaymentServices::burn_free_credit_for_para(¶_id)); + assert_ok!(PaymentServices::burn_block_production_free_credit_for_para( + ¶_id + )); assert_eq!(>::get(para_id), Some(0u64)); // now should fail assert_err!( - PaymentServices::burn_free_credit_for_para(¶_id), + PaymentServices::burn_block_production_free_credit_for_para(¶_id), + pallet_services_payment::Error::::InsufficientCredits, + ); + }); +} + +#[test] +fn burn_collator_assignment_credit_works() { + ExtBuilder::default() + .with_balances([(ALICE, 1_000)].into()) + .build() + .execute_with(|| { + let para_id = 1.into(); + assert_ok!(PaymentServices::set_collator_assignment_credits( + RuntimeOrigin::root(), + para_id, + 1u32, + ),); + + // should succeed and burn one + assert_eq!(>::get(para_id), Some(1u32)); + assert_ok!(PaymentServices::burn_collator_assignment_free_credit_for_para(¶_id)); + assert_eq!(>::get(para_id), Some(0u32)); + + // now should fail + assert_err!( + PaymentServices::burn_collator_assignment_free_credit_for_para(¶_id), pallet_services_payment::Error::::InsufficientCredits, ); }); @@ -123,60 +158,73 @@ fn burn_credit_fails_for_wrong_para() { .build() .execute_with(|| { let para_id = 1.into(); - assert_ok!(PaymentServices::set_credits( + assert_ok!(PaymentServices::set_block_production_credits( RuntimeOrigin::root(), para_id, 1u64, ),); + assert_ok!(PaymentServices::set_collator_assignment_credits( + RuntimeOrigin::root(), + para_id, + 1u32, + ),); // fails for wrong para let wrong_para_id = 2.into(); assert_err!( - PaymentServices::burn_free_credit_for_para(&wrong_para_id), + PaymentServices::burn_block_production_free_credit_for_para(&wrong_para_id), + pallet_services_payment::Error::::InsufficientCredits, + ); + assert_err!( + PaymentServices::burn_collator_assignment_free_credit_for_para(&wrong_para_id), pallet_services_payment::Error::::InsufficientCredits, ); }); } #[test] -fn set_credits_bad_origin() { +fn set_block_production_credits_bad_origin() { ExtBuilder::default() .with_balances([(ALICE, 1_000)].into()) .build() .execute_with(|| { assert_err!( - PaymentServices::set_credits(RuntimeOrigin::signed(ALICE), 1.into(), 1u64,), + PaymentServices::set_block_production_credits( + RuntimeOrigin::signed(ALICE), + 1.into(), + 1u64, + ), DispatchError::BadOrigin ) }); } #[test] -fn set_credits_above_max_works() { +fn set_block_production_credits_above_max_works() { ExtBuilder::default() .with_balances([(ALICE, 1_000)].into()) .build() .execute_with(|| { - assert_ok!(PaymentServices::set_credits( + assert_ok!(PaymentServices::set_block_production_credits( RuntimeOrigin::root(), 1.into(), - MaxCreditsStored::get() * 2, + FreeBlockProductionCredits::get() * 2, )); assert_eq!( >::get(ParaId::from(1)), - Some(MaxCreditsStored::get() * 2) + Some(FreeBlockProductionCredits::get() * 2) ); }); } #[test] -fn set_credits_to_zero_kills_storage() { +fn set_block_production_credits_to_zero_kills_storage() { ExtBuilder::default() .with_balances([(ALICE, 1_000)].into()) .build() .execute_with(|| { - assert_ok!(PaymentServices::set_credits( + assert_ok!(PaymentServices::set_block_production_credits( RuntimeOrigin::root(), 1.into(), 0u64, @@ -237,6 +285,13 @@ fn credits_should_not_be_substracted_from_tank_if_it_involves_death() { Balances::balance(&crate::Pallet::::parachain_tank(1.into())), 100u128 ); + + PaymentServices::on_collators_assigned(1.into()); + + assert_eq!( + Balances::balance(&crate::Pallet::::parachain_tank(1.into())), + 100u128 + ); }); } @@ -266,3 +321,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..79de682b3 100644 --- a/pallets/services-payment/src/weights.rs +++ b/pallets/services-payment/src/weights.rs @@ -56,6 +56,8 @@ pub trait WeightInfo { fn set_credits() -> Weight; fn set_given_free_credits() -> Weight; fn on_container_author_noted() -> Weight; + fn on_collators_assigned() -> Weight; + fn set_refund_address() -> Weight; } /// Weights for pallet_services_payment using the Substrate node and recommended hardware. @@ -108,6 +110,31 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } + /// Storage: `ServicesPayment::CollatorAssignmentCredits` (r:1 w:0) + /// Proof: `ServicesPayment::CollatorAssignmentCredits` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn on_collators_assigned() -> Weight { + // Proof Size summary in bytes: + // Measured: `258` + // Estimated: `3593` + // Minimum execution time: 18_648_000 picoseconds. + Weight::from_parts(19_211_000, 3593) + .saturating_add(T::DbWeight::get().reads(2_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 +185,29 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + /// Storage: `ServicesPayment::CollatorAssignmentCredits` (r:1 w:0) + /// Proof: `ServicesPayment::CollatorAssignmentCredits` (`max_values`: None, `max_size`: Some(24), added: 2499, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn on_collators_assigned() -> Weight { + // Proof Size summary in bytes: + // Measured: `258` + // Estimated: `3593` + // Minimum execution time: 18_648_000 picoseconds. + Weight::from_parts(19_211_000, 3593) + .saturating_add(RocksDbWeight::get().reads(2_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/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