From 4078d11c94f06de26618f775cc5d64bb6e7466b1 Mon Sep 17 00:00:00 2001 From: Gavin Wood Date: Sun, 14 Mar 2021 22:39:41 +0100 Subject: [PATCH] New slots/auctions architecture (#2294) * TODOs * Add auctions.rs, comment on changes needed. * Remove cruft from slots * Remove more from auctions.rs * More logic drafting in slots. * More logic in slots.rs * patch some errors * more fixes * last nit * Cleanups in slots.rs * Cleanups in slots.rs * patches * make build * crowdloan to new api * auction compile * Use ParaId instead of FundIndex in Crowdloan (#2303) * use paraid instead of fundindex * Update crowdloan.rs * check caller is manager * Auction tests and fix build warnings. * Configurable origin for initiating auctions * Remove on_finalize * #2303 (manual merge) * Tests for Slots * some registrar tests * Apply suggestions from code review Co-authored-by: Guillaume Thiolliere * Update runtime/common/src/slots.rs Co-authored-by: Guillaume Thiolliere * Slots uses Registrar for CurrentChains * swap works test * on swap impl * traitify parachain cleanup * explicit lifecycle tracking for paras * initial implementation of lifecycles and upgrades * clean up a bit * Update runtime/common/src/slots.rs Co-authored-by: Guillaume Thiolliere * fix doc comment * more rigid lifecycle checks * include paras which are transitioning, and lifecycle query * format guide * update api * update guide * explicit outgoing state, fix genesis * handle outgoing with transitioning paras * Revert "explicit lifecycle tracking for paras" This reverts commit 4177af7ba473bbd9c26bccd861793f25265b6657. * remove lifecycle tracking from registrar * do not include transitioning paras in identifier * Update paras_registrar.rs * final patches to registrar * Fix test * use noop in test * clean up pending swap on deregistration * finish registrar tests * Update roadmap/implementers-guide/src/runtime/paras.md * Update roadmap/implementers-guide/src/runtime/paras.md * Update roadmap/implementers-guide/src/runtime/paras.md * Apply suggestions from code review * Use matches macro * Correct terms * Apply suggestions from code review * Remove direct need for Slots and Registrar from Crowdloan * Rejig things slightly * actions queue * Revert "actions queue" This reverts commit b2e9011ec8937d6c73e99292416c9692aeb30f73. * Traitify Auction interface. * Mockups and initial code for Crowdloan testing * One test... * collapse onboarding state * fix some crowdloan tests * one more * start benchmarks for auctions * benchmark bid * fix more crowdloan tests * onboard and begin retirement no longer exist * Revert "onboard and begin retirement no longer exist" This reverts commit 2e100fd94e3540bff5f172328b5d917896f1c6fc. * Simplify crowdloan and make it work. * Fixes * fix some * finish merge fixes * fix refund bug in auctions * Add traits to Registrar for tests and benchmarks * fix more auction benchmarks * Fix TestAuctioneer * finish crowdloan benchmarks * start setting up full integration tests * expand integration tests * finish basic integration test * add more integration tests * begin slots benchmarks * start paras registrar benchmarks * fix merge * fix tests * clean up paras registrar * remove println * remove outdated cleanup config * update benchmarks * Add WeightInfo * enable runtime-benchmarks feature flag * complete swap benchmark * add parachains and onboarding into westend * add benchmarks and genesis * cargo run --release --features=runtime-benchmarks -- benchmark --chain=westend-dev --steps=50 --repeat=20 --pallet=auctions --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --header=./file_header.txt --output=./runtime/westend/src/weights/ * cargo run --release --features=runtime-benchmarks -- benchmark --chain=westend-dev --steps=50 --repeat=20 --pallet=slots --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --header=./file_header.txt --output=./runtime/westend/src/weights/ * fix benchmark execution * cargo run --release --features=runtime-benchmarks -- benchmark --chain=westend-dev --steps=50 --repeat=20 --pallet=crowdloan --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --header=./file_header.txt --output=./runtime/westend/src/weights/ * cargo run --release --features=runtime-benchmarks -- benchmark --chain=westend-dev --steps=50 --repeat=20 --pallet=paras_registrar --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --header=./file_header.txt --output=./runtime/westend/src/weights/ * Use `new_raise_len` in crowdloan on_initialize * Update paras_registrar.rs * fix westend merge * impl on_swap for crowdloan * Check fund exists before create * update for crowdloan sig * cargo run --release --features=runtime-benchmarks -- benchmark --chain=westend-dev --steps=50 --repeat=20 --pallet=crowdloan --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --header=./file_header.txt --output=./runtime/westend/src/weights/ * slots on_initialize * use integration tests environment for benchmarks * fix hrmp event * auction on_initialize * cargo run --release --features=runtime-benchmarks -- benchmark --chain=westend-dev --steps=50 --repeat=20 --pallet=auctions --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --header=./file_header.txt --output=./runtime/westend/src/weights/ * fix storage name in auctions * add auction_index to winning data * winning data takes into account current auction index * remove println * cargo run --release --features=runtime-benchmarks -- benchmark --chain=westend-dev --steps=50 --repeat=20 --pallet=auctions --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --header=./file_header.txt --output=./runtime/westend/src/weights/ * cargo run --release --features=runtime-benchmarks -- benchmark --chain=westend-dev --steps=50 --repeat=20 --pallet=slots --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --header=./file_header.txt --output=./runtime/westend/src/weights/ * Revert "add auction_index to winning data" * PastRandomness. * Fixes * Use new randomness * fix use of randomness in auctions and runtime config * expose consts * fix auction test * add deposit per byte for para registration * basic swap integration test * make swap test more comprehensive * Add WinningVec for easier retrieval in the front-end. * clean up `WinningVec` at the end * Add event for when a new best bid comes in * Fix propagation of winners in ending period * fix benchmarks, refund weight in dissolve * fix unused * remove some TODOs * setup opaque keys for paras in westend * cargo run --release --features=runtime-benchmarks -- benchmark --chain=westend-dev --steps=50 --repeat=20 --pallet=crowdloan --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --header=./file_header.txt --output=./runtime/westend/src/weights/ * remove unused * cargo run --release --features=runtime-benchmarks -- benchmark --chain=westend-dev --steps=50 --repeat=20 --pallet=auctions --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --header=./file_header.txt --output=./runtime/westend/src/weights/ * back to regular runtime config * use saturating math where user input can be * better first slot check * Update runtime/common/src/claims.rs * update westend onswap impl Co-authored-by: Shawn Tabrizi Co-authored-by: Guillaume Thiolliere Co-authored-by: Parity Benchmarking Bot --- Cargo.lock | 2 + node/service/src/chain_spec.rs | 82 + runtime/common/Cargo.toml | 7 +- runtime/common/src/auctions.rs | 1450 +++++++++++++ runtime/common/src/crowdloan.rs | 1541 +++++--------- runtime/common/src/integration_tests.rs | 882 ++++++++ runtime/common/src/lib.rs | 6 + runtime/common/src/mock.rs | 184 ++ runtime/common/src/paras_registrar.rs | 1073 ++++++---- runtime/common/src/slots.rs | 1873 ++++------------- runtime/common/src/traits.rs | 182 ++ runtime/parachains/src/lib.rs | 13 +- runtime/parachains/src/paras.rs | 9 + runtime/parachains/src/shared.rs | 7 +- runtime/rococo/src/lib.rs | 22 +- runtime/rococo/src/propose_parachain.rs | 4 - runtime/westend/Cargo.toml | 1 + runtime/westend/src/lib.rs | 159 +- runtime/westend/src/weights/auctions.rs | 61 + runtime/westend/src/weights/crowdloan.rs | 78 + .../westend/src/weights/paras_registrar.rs | 61 + runtime/westend/src/weights/slots.rs | 64 + 22 files changed, 4906 insertions(+), 2855 deletions(-) create mode 100644 runtime/common/src/auctions.rs create mode 100644 runtime/common/src/integration_tests.rs create mode 100644 runtime/common/src/mock.rs create mode 100644 runtime/common/src/traits.rs create mode 100644 runtime/westend/src/weights/auctions.rs create mode 100644 runtime/westend/src/weights/crowdloan.rs create mode 100644 runtime/westend/src/weights/paras_registrar.rs create mode 100644 runtime/westend/src/weights/slots.rs diff --git a/Cargo.lock b/Cargo.lock index f15b3f747253..cbbdd06c1e33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5960,6 +5960,7 @@ dependencies = [ "frame-support-test", "frame-system", "hex-literal", + "impl-trait-for-tuples", "libsecp256k1", "log", "pallet-authorship", @@ -10741,6 +10742,7 @@ dependencies = [ "polkadot-parachain", "polkadot-primitives", "polkadot-runtime-common", + "polkadot-runtime-parachains", "rustc-hex", "serde", "serde_derive", diff --git a/node/service/src/chain_spec.rs b/node/service/src/chain_spec.rs index bbacf9aa1752..62ba1c0d663e 100644 --- a/node/service/src/chain_spec.rs +++ b/node/service/src/chain_spec.rs @@ -462,6 +462,47 @@ fn westend_staging_testnet_config_genesis(wasm_binary: &[u8]) -> westend::Genesi pallet_sudo: westend::SudoConfig { key: endowed_accounts[0].clone(), }, + parachains_configuration: westend_runtime::ParachainsConfigurationConfig { + config: polkadot_runtime_parachains::configuration::HostConfiguration { + validation_upgrade_frequency: 600u32, + validation_upgrade_delay: 300, + acceptance_period: 1200, + max_code_size: 5 * 1024 * 1024, + max_pov_size: 50 * 1024 * 1024, + max_head_data_size: 32 * 1024, + group_rotation_frequency: 20, + chain_availability_period: 4, + thread_availability_period: 4, + max_upward_queue_count: 8, + max_upward_queue_size: 8 * 1024, + max_downward_message_size: 1024, + // this is approximatelly 4ms. + // + // Same as `4 * frame_support::weights::WEIGHT_PER_MILLIS`. We don't bother with + // an import since that's a made up number and should be replaced with a constant + // obtained by benchmarking anyway. + preferred_dispatchable_upward_messages_step_weight: 4 * 1_000_000_000, + max_upward_message_size: 1024, + max_upward_message_num_per_candidate: 5, + hrmp_open_request_ttl: 5, + hrmp_sender_deposit: 0, + hrmp_recipient_deposit: 0, + hrmp_channel_max_capacity: 8, + hrmp_channel_max_total_size: 8 * 1024, + hrmp_max_parachain_inbound_channels: 4, + hrmp_max_parathread_inbound_channels: 4, + hrmp_channel_max_message_size: 1024, + hrmp_max_parachain_outbound_channels: 4, + hrmp_max_parathread_outbound_channels: 4, + hrmp_max_message_num_per_candidate: 5, + no_show_slots: 2, + n_delay_tranches: 25, + needed_approvals: 2, + relay_vrf_modulo_samples: 10, + zeroth_delay_tranche_width: 0, + ..Default::default() + }, + }, } } @@ -1336,6 +1377,47 @@ pub fn westend_testnet_genesis( pallet_authority_discovery: westend::AuthorityDiscoveryConfig { keys: vec![] }, pallet_vesting: westend::VestingConfig { vesting: vec![] }, pallet_sudo: westend::SudoConfig { key: root_key }, + parachains_configuration: westend_runtime::ParachainsConfigurationConfig { + config: polkadot_runtime_parachains::configuration::HostConfiguration { + validation_upgrade_frequency: 600u32, + validation_upgrade_delay: 300, + acceptance_period: 1200, + max_code_size: 5 * 1024 * 1024, + max_pov_size: 50 * 1024 * 1024, + max_head_data_size: 32 * 1024, + group_rotation_frequency: 20, + chain_availability_period: 4, + thread_availability_period: 4, + max_upward_queue_count: 8, + max_upward_queue_size: 8 * 1024, + max_downward_message_size: 1024, + // this is approximatelly 4ms. + // + // Same as `4 * frame_support::weights::WEIGHT_PER_MILLIS`. We don't bother with + // an import since that's a made up number and should be replaced with a constant + // obtained by benchmarking anyway. + preferred_dispatchable_upward_messages_step_weight: 4 * 1_000_000_000, + max_upward_message_size: 1024, + max_upward_message_num_per_candidate: 5, + hrmp_open_request_ttl: 5, + hrmp_sender_deposit: 0, + hrmp_recipient_deposit: 0, + hrmp_channel_max_capacity: 8, + hrmp_channel_max_total_size: 8 * 1024, + hrmp_max_parachain_inbound_channels: 4, + hrmp_max_parathread_inbound_channels: 4, + hrmp_channel_max_message_size: 1024, + hrmp_max_parachain_outbound_channels: 4, + hrmp_max_parathread_outbound_channels: 4, + hrmp_max_message_num_per_candidate: 5, + no_show_slots: 2, + n_delay_tranches: 25, + needed_approvals: 2, + relay_vrf_modulo_samples: 10, + zeroth_delay_tranche_width: 0, + ..Default::default() + }, + }, } } diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index 55c3b309733c..3b3c62af83fd 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -5,6 +5,7 @@ authors = ["Parity Technologies "] edition = "2018" [dependencies] +impl-trait-for-tuples = "0.2.0" bitvec = { version = "0.20.1", default-features = false, features = ["alloc"] } parity-scale-codec = { version = "2.0.0", default-features = false, features = ["derive"] } log = { version = "0.4.13", optional = true } @@ -33,7 +34,9 @@ pallet-vesting = { git = "https://github.com/paritytech/substrate", branch = "ma pallet-offences = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } pallet-transaction-payment = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } pallet-treasury = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } + frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false, optional = true } +pallet-babe = { git = "https://github.com/paritytech/substrate", branch = "master", default-features=false, optional = true } primitives = { package = "polkadot-primitives", path = "../../primitives", default-features = false } libsecp256k1 = { version = "0.3.5", default-features = false, optional = true } @@ -45,10 +48,10 @@ xcm = { path = "../../xcm", default-features = false } hex-literal = "0.3.1" keyring = { package = "sp-keyring", git = "https://github.com/paritytech/substrate", branch = "master" } sp-trie = { git = "https://github.com/paritytech/substrate", branch = "master" } -pallet-babe = { git = "https://github.com/paritytech/substrate", branch = "master" } sp-application-crypto = { git = "https://github.com/paritytech/substrate", branch = "master" } sp-election-providers = { git = "https://github.com/paritytech/substrate", branch = "master" } frame-support-test = { git = "https://github.com/paritytech/substrate", branch = "master" } +pallet-babe = { git = "https://github.com/paritytech/substrate", branch = "master" } pallet-randomness-collective-flip = { git = "https://github.com/paritytech/substrate", branch = "master" } pallet-staking-reward-curve = { git = "https://github.com/paritytech/substrate", branch = "master" } pallet-treasury = { git = "https://github.com/paritytech/substrate", branch = "master" } @@ -93,4 +96,6 @@ runtime-benchmarks = [ "frame-benchmarking", "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", + "runtime-parachains/runtime-benchmarks", + "pallet-babe/runtime-benchmarks", ] diff --git a/runtime/common/src/auctions.rs b/runtime/common/src/auctions.rs new file mode 100644 index 000000000000..63f9fd14aa3b --- /dev/null +++ b/runtime/common/src/auctions.rs @@ -0,0 +1,1450 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! Auctioning system to determine the set of Parachains in operation. This includes logic for the +//! auctioning mechanism and for reserving balance as part of the "payment". Unreserving the balance +//! happens elsewhere. + +use sp_std::{prelude::*, mem::swap, convert::TryInto}; +use sp_runtime::traits::{CheckedSub, Zero, One, Saturating}; +use frame_support::{ + decl_module, decl_storage, decl_event, decl_error, ensure, dispatch::DispatchResult, + traits::{Randomness, Currency, ReservableCurrency, Get, EnsureOrigin}, + weights::{DispatchClass, Weight}, +}; +use primitives::v1::Id as ParaId; +use frame_system::ensure_signed; +use crate::slot_range::{SlotRange, SLOT_RANGE_COUNT}; +use crate::traits::{Leaser, LeaseError, Auctioneer}; +use parity_scale_codec::Decode; + +type CurrencyOf = <::Leaser as Leaser>::Currency; +type BalanceOf = <<::Leaser as Leaser>::Currency as Currency<::AccountId>>::Balance; + +pub trait WeightInfo { + fn new_auction() -> Weight; + fn bid() -> Weight; + fn on_initialize() -> Weight; +} + +pub struct TestWeightInfo; +impl WeightInfo for TestWeightInfo { + fn new_auction() -> Weight { 0 } + fn bid() -> Weight { 0 } + fn on_initialize() -> Weight { 0 } +} + +/// The module's configuration trait. +pub trait Config: frame_system::Config { + /// The overarching event type. + type Event: From> + Into<::Event>; + + /// The number of blocks over which a single period lasts. + type Leaser: Leaser; + + /// The number of blocks over which an auction may be retroactively ended. + type EndingPeriod: Get; + + /// Something that provides randomness in the runtime. + type Randomness: Randomness; + + /// The origin which may initiate auctions. + type InitiateOrigin: EnsureOrigin; + + /// Weight Information for the Extrinsics in the Pallet + type WeightInfo: WeightInfo; +} + +/// An auction index. We count auctions in this type. +pub type AuctionIndex = u32; + +type LeasePeriodOf = <::Leaser as Leaser>::LeasePeriod; +// Winning data type. This encodes the top bidders of each range together with their bid. +type WinningData = + [Option<(::AccountId, ParaId, BalanceOf)>; SLOT_RANGE_COUNT]; +// Winners data type. This encodes each of the final winners of a parachain auction, the parachain +// index assigned to them, their winning bid and the range that they won. +type WinnersData = Vec<(::AccountId, ParaId, BalanceOf, SlotRange)>; + +// This module's storage items. +decl_storage! { + trait Store for Module as Auctions { + /// Number of auctions started so far. + pub AuctionCounter: AuctionIndex; + + /// Information relating to the current auction, if there is one. + /// + /// The first item in the tuple is the lease period index that the first of the four + /// contiguous lease periods on auction is for. The second is the block number when the + /// auction will "begin to end", i.e. the first block of the Ending Period of the auction. + pub AuctionInfo get(fn auction_info): Option<(LeasePeriodOf, T::BlockNumber)>; + + /// Amounts currently reserved in the accounts of the bidders currently winning + /// (sub-)ranges. + pub ReservedAmounts get(fn reserved_amounts): + map hasher(twox_64_concat) (T::AccountId, ParaId) => Option>; + + /// The winning bids for each of the 10 ranges at each block in the final Ending Period of + /// the current auction. The map's key is the 0-based index into the Ending Period. The + /// first block of the ending period is 0; the last is `EndingPeriod - 1`. + pub Winning get(fn winning): map hasher(twox_64_concat) T::BlockNumber => Option>; + } +} + +decl_event!( + pub enum Event where + AccountId = ::AccountId, + BlockNumber = ::BlockNumber, + LeasePeriod = LeasePeriodOf, + ParaId = ParaId, + Balance = BalanceOf, + { + /// An auction started. Provides its index and the block number where it will begin to + /// close and the first lease period of the quadruplet that is auctioned. + /// [auction_index, lease_period, ending] + AuctionStarted(AuctionIndex, LeasePeriod, BlockNumber), + /// An auction ended. All funds become unreserved. [auction_index] + AuctionClosed(AuctionIndex), + /// Someone won the right to deploy a parachain. Balance amount is deducted for deposit. + /// [bidder, range, parachain_id, amount] + WonDeploy(AccountId, SlotRange, ParaId, Balance), + /// An existing parachain won the right to continue. + /// First balance is the extra amount reseved. Second is the total amount reserved. + /// [parachain_id, begin, count, total_amount] + WonRenewal(ParaId, LeasePeriod, LeasePeriod, Balance), + /// Funds were reserved for a winning bid. First balance is the extra amount reserved. + /// Second is the total. [bidder, extra_reserved, total_amount] + Reserved(AccountId, Balance, Balance), + /// Funds were unreserved since bidder is no longer active. [bidder, amount] + Unreserved(AccountId, Balance), + /// Someone attempted to lease the same slot twice for a parachain. The amount is held in reserve + /// but no parachain slot has been leased. + /// \[parachain_id, leaser, amount\] + ReserveConfiscated(ParaId, AccountId, Balance), + /// A new bid has been accepted as the current winner. + /// \[who, para_id, amount, first_slot, last_slot\] + BidAccepted(AccountId, ParaId, Balance, LeasePeriod, LeasePeriod), + } +); + +decl_error! { + pub enum Error for Module { + /// This auction is already in progress. + AuctionInProgress, + /// The lease period is in the past. + LeasePeriodInPast, + /// The origin for this call must be a parachain. + NotParaOrigin, + /// The parachain ID is not on-boarding. + ParaNotOnboarding, + /// The origin for this call must be the origin who registered the parachain. + InvalidOrigin, + /// Parachain is already registered. + AlreadyRegistered, + /// The code must correspond to the hash. + InvalidCode, + /// Deployment data has not been set for this parachain. + UnsetDeployData, + /// The bid must overlap all intersecting ranges. + NonIntersectingRange, + /// Not a current auction. + NotCurrentAuction, + /// Not an auction. + NotAuction, + /// Given code size is too large. + CodeTooLarge, + /// Given initial head data is too large. + HeadDataTooLarge, + /// Auction has already ended. + AuctionEnded, + } +} + +decl_module! { + pub struct Module for enum Call where origin: T::Origin { + type Error = Error; + + const EndingPeriod: T::BlockNumber = T::EndingPeriod::get(); + + fn deposit_event() = default; + + fn on_initialize(n: T::BlockNumber) -> Weight { + let mut weight = T::DbWeight::get().reads(1); + + // If the current auction was in its ending period last block, then ensure that the (sub-)range + // winner information is duplicated from the previous block in case no bids happened in the + // last block. + if let Some(offset) = Self::is_ending(n) { + weight = weight.saturating_add(T::DbWeight::get().reads(1)); + if !Winning::::contains_key(&offset) { + weight = weight.saturating_add(T::DbWeight::get().writes(1)); + let winning_data = offset.checked_sub(&One::one()) + .and_then(Winning::::get) + .unwrap_or_default(); + Winning::::insert(offset, winning_data); + } + } + + // Check to see if an auction just ended. + if let Some((winning_ranges, auction_lease_period_index)) = Self::check_auction_end(n) { + // Auction is ended now. We have the winning ranges and the lease period index which + // acts as the offset. Handle it. + Self::manage_auction_end( + auction_lease_period_index, + winning_ranges, + ); + weight = weight.saturating_add(T::WeightInfo::on_initialize()); + } + + weight + } + + /// Create a new auction. + /// + /// This can only happen when there isn't already an auction in progress and may only be + /// called by the root origin. Accepts the `duration` of this auction and the + /// `lease_period_index` of the initial lease period of the four that are to be auctioned. + #[weight = (T::WeightInfo::new_auction(), DispatchClass::Operational)] + pub fn new_auction(origin, + #[compact] duration: T::BlockNumber, + #[compact] lease_period_index: LeasePeriodOf, + ) { + T::InitiateOrigin::ensure_origin(origin)?; + + ensure!(!Self::is_in_progress(), Error::::AuctionInProgress); + ensure!(lease_period_index >= T::Leaser::lease_period_index(), Error::::LeasePeriodInPast); + + // Bump the counter. + let n = AuctionCounter::mutate(|n| { *n += 1; *n }); + + // Set the information. + let ending = frame_system::Module::::block_number().saturating_add(duration); + AuctionInfo::::put((lease_period_index, ending)); + + Self::deposit_event(RawEvent::AuctionStarted(n, lease_period_index, ending)) + } + + /// Make a new bid from an account (including a parachain account) for deploying a new + /// parachain. + /// + /// Multiple simultaneous bids from the same bidder are allowed only as long as all active + /// bids overlap each other (i.e. are mutually exclusive). Bids cannot be redacted. + /// + /// - `sub` is the sub-bidder ID, allowing for multiple competing bids to be made by (and + /// funded by) the same account. + /// - `auction_index` is the index of the auction to bid on. Should just be the present + /// value of `AuctionCounter`. + /// - `first_slot` is the first lease period index of the range to bid on. This is the + /// absolute lease period index value, not an auction-specific offset. + /// - `last_slot` is the last lease period index of the range to bid on. This is the + /// absolute lease period index value, not an auction-specific offset. + /// - `amount` is the amount to bid to be held as deposit for the parachain should the + /// bid win. This amount is held throughout the range. + #[weight = T::WeightInfo::bid()] + pub fn bid(origin, + #[compact] para: ParaId, + #[compact] auction_index: AuctionIndex, + #[compact] first_slot: LeasePeriodOf, + #[compact] last_slot: LeasePeriodOf, + #[compact] amount: BalanceOf + ) { + let who = ensure_signed(origin)?; + Self::handle_bid(who, para, auction_index, first_slot, last_slot, amount)?; + } + } +} + +impl Auctioneer for Module { + type AccountId = T::AccountId; + type BlockNumber = T::BlockNumber; + type LeasePeriod = T::BlockNumber; + type Currency = CurrencyOf; + + fn new_auction( + duration: T::BlockNumber, + lease_period_index: LeasePeriodOf, + ) -> DispatchResult { + Self::do_new_auction(duration, lease_period_index) + } + + fn is_ending(now: Self::BlockNumber) -> Option { + if let Some((_, early_end)) = AuctionInfo::::get() { + if let Some(after_early_end) = now.checked_sub(&early_end) { + if after_early_end < T::EndingPeriod::get() { + return Some(after_early_end) + } + } + } + None + } + + fn place_bid( + bidder: T::AccountId, + para: ParaId, + first_slot: LeasePeriodOf, + last_slot: LeasePeriodOf, + amount: BalanceOf, + ) -> DispatchResult { + Self::handle_bid(bidder, para, AuctionCounter::get(), first_slot, last_slot, amount) + } + + fn lease_period_index() -> Self::LeasePeriod { + T::Leaser::lease_period_index() + } +} + +impl Module { + /// True if an auction is in progress. + pub fn is_in_progress() -> bool { + AuctionInfo::::get().map_or(false, |(_, early_end)| { + let late_end = early_end.saturating_add(T::EndingPeriod::get()); + // We need to check that the auction isn't in the period where it has definitely ended, but yeah we keep the + // info around because we haven't yet decided *exactly* when in the `EndingPeriod` that it ended. + let now = frame_system::Module::::block_number(); + now < late_end + }) + } + + /// Create a new auction. + /// + /// This can only happen when there isn't already an auction in progress. Accepts the `duration` + /// of this auction and the `lease_period_index` of the initial lease period of the four that + /// are to be auctioned. + fn do_new_auction( + duration: T::BlockNumber, + lease_period_index: LeasePeriodOf, + ) -> DispatchResult { + ensure!(!Self::is_in_progress(), Error::::AuctionInProgress); + ensure!(lease_period_index >= T::Leaser::lease_period_index(), Error::::LeasePeriodInPast); + + // Bump the counter. + let n = AuctionCounter::mutate(|n| { *n += 1; *n }); + + // Set the information. + let ending = frame_system::Module::::block_number().saturating_add(duration); + AuctionInfo::::put((lease_period_index, ending)); + + Self::deposit_event(RawEvent::AuctionStarted(n, lease_period_index, ending)); + Ok(()) + } + + /// Actually place a bid in the current auction. + /// + /// - `bidder`: The account that will be funding this bid. + /// - `auction_index`: The auction index of the bid. For this to succeed, must equal + /// the current value of `AuctionCounter`. + /// - `first_slot`: The first lease period index of the range to be bid on. + /// - `last_slot`: The last lease period index of the range to be bid on (inclusive). + /// - `amount`: The total amount to be the bid for deposit over the range. + pub fn handle_bid( + bidder: T::AccountId, + para: ParaId, + auction_index: u32, + first_slot: LeasePeriodOf, + last_slot: LeasePeriodOf, + amount: BalanceOf, + ) -> DispatchResult { + // Bidding on latest auction. + ensure!(auction_index == AuctionCounter::get(), Error::::NotCurrentAuction); + // Assume it's actually an auction (this should never fail because of above). + let (first_lease_period, early_end) = AuctionInfo::::get().ok_or(Error::::NotAuction)?; + let late_end = early_end.saturating_add(T::EndingPeriod::get()); + + // We need to check that the auction isn't in the period where it has definitely ended, but yeah we keep the + // info around because we haven't yet decided *exactly* when in the `EndingPeriod` that it ended. + let now = frame_system::Module::::block_number(); + ensure!(now < late_end, Error::::AuctionEnded); + + // Our range. + let range = SlotRange::new_bounded(first_lease_period, first_slot, last_slot)?; + // Range as an array index. + let range_index = range as u8 as usize; + // The offset into the auction ending set. + let offset = Self::is_ending(frame_system::Module::::block_number()).unwrap_or_default(); + // The current winning ranges. + let mut current_winning = Winning::::get(offset) + .or_else(|| offset.checked_sub(&One::one()).and_then(Winning::::get)) + .unwrap_or_default(); + + // If this bid beat the previous winner of our range. + if current_winning[range_index].as_ref().map_or(true, |last| amount > last.2) { + // This must overlap with all existing ranges that we're winning on or it's invalid. + ensure!(current_winning.iter() + .enumerate() + .all(|(i, x)| x.as_ref().map_or(true, |(w, _, _)| + w != &bidder || range.intersects(i.try_into() + .expect("array has SLOT_RANGE_COUNT items; index never reaches that value; qed") + ) + )), + Error::::NonIntersectingRange, + ); + + // Ok; we are the new winner of this range - reserve the additional amount and record. + + // Get the amount already held on deposit if this is a renewal bid (i.e. there's + // an existing lease on the same para by the same leaser). + let existing_lease_deposit = T::Leaser::deposit_held(para, &bidder); + let reserve_required = amount.saturating_sub(existing_lease_deposit); + + // Get the amount already reserved in any prior and still active bids by us. + let bidder_para = (bidder.clone(), para); + let already_reserved = ReservedAmounts::::get(&bidder_para).unwrap_or_default(); + + // If these don't already cover the bid... + if let Some(additional) = reserve_required.checked_sub(&already_reserved) { + // ...then reserve some more funds from their account, failing if there's not + // enough funds. + CurrencyOf::::reserve(&bidder, additional)?; + // ...and record the amount reserved. + ReservedAmounts::::insert(&bidder_para, reserve_required); + + Self::deposit_event(RawEvent::Reserved( + bidder.clone(), + additional, + reserve_required, + )); + } + + // Return any funds reserved for the previous winner if they no longer have any active + // bids. + let mut outgoing_winner = Some((bidder.clone(), para, amount)); + swap(&mut current_winning[range_index], &mut outgoing_winner); + if let Some((who, para, _amount)) = outgoing_winner { + if current_winning.iter() + .filter_map(Option::as_ref) + .all(|&(ref other, other_para, _)| other != &who || other_para != para) + { + // Previous bidder is no longer winning any ranges: unreserve their funds. + if let Some(amount) = ReservedAmounts::::take(&(who.clone(), para)) { + // It really should be reserved; there's not much we can do here on fail. + let err_amt = CurrencyOf::::unreserve(&who, amount); + debug_assert!(err_amt.is_zero()); + + Self::deposit_event(RawEvent::Unreserved(who, amount)); + } + } + } + + // Update the range winner. + Winning::::insert(offset, ¤t_winning); + Self::deposit_event(RawEvent::BidAccepted(bidder, para, amount, first_slot, last_slot)); + } + Ok(()) + } + + /// Some when the auction's end is known (with the end block number). None if it is unknown. + /// If `Some` then the block number must be at most the previous block and at least the + /// previous block minus `T::EndingPeriod::get()`. + /// + /// This mutates the state, cleaning up `AuctionInfo` and `Winning` in the case of an auction + /// ending. An immediately subsequent call with the same argument will always return `None`. + fn check_auction_end(now: T::BlockNumber) -> Option<(WinningData, LeasePeriodOf)> { + if let Some((lease_period_index, early_end)) = AuctionInfo::::get() { + let ending_period = T::EndingPeriod::get(); + let late_end = early_end.saturating_add(ending_period); + let is_ended = now >= late_end; + if is_ended { + // auction definitely ended. + // check to see if we can determine the actual ending point. + let (raw_offset, known_since) = T::Randomness::random(&b"para_auction"[..]); + + if late_end <= known_since { + // Our random seed was known only after the auction ended. Good to use. + let raw_offset_block_number = ::decode(&mut raw_offset.as_ref()) + .expect("secure hashes should always be bigger than the block number; qed"); + let offset = raw_offset_block_number % ending_period; + let res = Winning::::get(offset).unwrap_or_default(); + let mut i = T::BlockNumber::zero(); + while i < ending_period { + Winning::::remove(i); + i += One::one(); + } + AuctionInfo::::kill(); + return Some((res, lease_period_index)) + } + } + } + None + } + + /// Auction just ended. We have the current lease period, the auction's lease period (which + /// is guaranteed to be at least the current period) and the bidders that were winning each + /// range at the time of the auction's close. + fn manage_auction_end( + auction_lease_period_index: LeasePeriodOf, + winning_ranges: WinningData, + ) { + // First, unreserve all amounts that were reserved for the bids. We will later re-reserve the + // amounts from the bidders that ended up being assigned the slot so there's no need to + // special-case them here. + for ((bidder, para), amount) in ReservedAmounts::::iter() { + ReservedAmounts::::take((bidder.clone(), para)); + CurrencyOf::::unreserve(&bidder, amount); + } + + // Next, calculate the winning combination of slots and thus the final winners of the + // auction. + let winners = Self::calculate_winners(winning_ranges); + + // Go through those winners and re-reserve their bid, updating our table of deposits + // accordingly. + for (leaser, para, amount, range) in winners.into_iter() { + let begin_offset = LeasePeriodOf::::from(range.as_pair().0 as u32); + let period_begin = auction_lease_period_index + begin_offset; + let period_count = LeasePeriodOf::::from(range.len() as u32); + + match T::Leaser::lease_out(para, &leaser, amount, period_begin, period_count) { + Err(LeaseError::ReserveFailed) | Err(LeaseError::AlreadyEnded) => { + // Should never happen since we just unreserved this amount (and our offset is from the + // present period). But if it does, there's not much we can do. + } + Err(LeaseError::AlreadyLeased) => { + // The leaser attempted to get a second lease on the same para ID, possibly griefing us. Let's + // keep the amount reserved and let governance sort it out. + if CurrencyOf::::reserve(&leaser, amount).is_ok() { + Self::deposit_event(RawEvent::ReserveConfiscated(para, leaser, amount)); + } + } + Ok(()) => {}, // Nothing to report. + } + } + + Self::deposit_event(RawEvent::AuctionClosed(AuctionCounter::get())); + } + + /// Calculate the final winners from the winning slots. + /// + /// This is a simple dynamic programming algorithm designed by Al, the original code is at: + /// https://github.com/w3f/consensus/blob/master/NPoS/auctiondynamicthing.py + fn calculate_winners( + mut winning: WinningData + ) -> WinnersData { + let winning_ranges = { + let mut best_winners_ending_at: + [(Vec, BalanceOf); 4] = Default::default(); + let best_bid = |range: SlotRange| { + winning[range as u8 as usize].as_ref() + .map(|(_, _, amount)| *amount * (range.len() as u32).into()) + }; + for i in 0..4 { + let r = SlotRange::new_bounded(0, 0, i as u32).expect("`i < 4`; qed"); + if let Some(bid) = best_bid(r) { + best_winners_ending_at[i] = (vec![r], bid); + } + for j in 0..i { + let r = SlotRange::new_bounded(0, j as u32 + 1, i as u32) + .expect("`i < 4`; `j < i`; `j + 1 < 4`; qed"); + if let Some(mut bid) = best_bid(r) { + bid += best_winners_ending_at[j].1; + if bid > best_winners_ending_at[i].1 { + let mut new_winners = best_winners_ending_at[j].0.clone(); + new_winners.push(r); + best_winners_ending_at[i] = (new_winners, bid); + } + } else { + if best_winners_ending_at[j].1 > best_winners_ending_at[i].1 { + best_winners_ending_at[i] = best_winners_ending_at[j].clone(); + } + } + } + } + let [_, _, _, (winning_ranges, _)] = best_winners_ending_at; + winning_ranges + }; + + winning_ranges.into_iter().map(|range| { + let mut final_winner = Default::default(); + swap(&mut final_winner, winning[range as u8 as usize].as_mut() + .expect("none values are filtered out in previous logic; qed")); + let (bidder, para, amount) = final_winner; + (bidder, para, amount, range) + }).collect::>() + } +} + +/// tests for this module +#[cfg(test)] +mod tests { + use super::*; + use std::{collections::BTreeMap, cell::RefCell}; + use sp_core::H256; + use sp_runtime::traits::{BlakeTwo256, IdentityLookup}; + use frame_support::{ + parameter_types, ord_parameter_types, assert_ok, assert_noop, assert_storage_noop, + traits::{OnInitialize, OnFinalize}, + dispatch::DispatchError::BadOrigin, + }; + use frame_system::{EnsureSignedBy, EnsureOneOf, EnsureRoot}; + use pallet_balances; + use crate::auctions; + use primitives::v1::{BlockNumber, Header, Id as ParaId}; + + type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; + type Block = frame_system::mocking::MockBlock; + + frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Module, Call, Config, Storage, Event}, + Balances: pallet_balances::{Module, Call, Storage, Config, Event}, + Auctions: auctions::{Module, Call, Storage, Event}, + } + ); + + parameter_types! { + pub const BlockHashCount: u32 = 250; + } + impl frame_system::Config for Test { + type BaseCallFilter = (); + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + } + + parameter_types! { + pub const ExistentialDeposit: u64 = 1; + } + + impl pallet_balances::Config for Test { + type Balance = u64; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = (); + } + + #[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Copy, Debug)] + pub struct LeaseData { + leaser: u64, + amount: u64, + } + + thread_local! { + pub static LEASES: + RefCell> = RefCell::new(BTreeMap::new()); + } + + fn leases() -> Vec<((ParaId, BlockNumber), LeaseData)> { + LEASES.with(|p| (&*p.borrow()).clone().into_iter().collect::>()) + } + + pub struct TestLeaser; + impl Leaser for TestLeaser { + type AccountId = u64; + type LeasePeriod = BlockNumber; + type Currency = Balances; + + fn lease_out( + para: ParaId, + leaser: &Self::AccountId, + amount: >::Balance, + period_begin: Self::LeasePeriod, + period_count: Self::LeasePeriod, + ) -> Result<(), LeaseError> { + LEASES.with(|l| { + let mut leases = l.borrow_mut(); + if period_begin < Self::lease_period_index() { + return Err(LeaseError::AlreadyEnded) + } + for period in period_begin..(period_begin + period_count) { + if leases.contains_key(&(para, period)) { + return Err(LeaseError::AlreadyLeased) + } + leases.insert((para, period), LeaseData { leaser: leaser.clone(), amount }); + } + Ok(()) + }) + } + + fn deposit_held(para: ParaId, leaser: &Self::AccountId) -> >::Balance { + leases().iter() + .filter_map(|((id, _period), data)| + if id == ¶ && &data.leaser == leaser { Some(data.amount) } else { None } + ) + .max() + .unwrap_or_default() + } + + fn lease_period() -> Self::LeasePeriod { + 10 + } + + fn lease_period_index() -> Self::LeasePeriod { + (System::block_number() / Self::lease_period()).into() + } + } + + parameter_types!{ + pub const EndingPeriod: BlockNumber = 3; + } + + ord_parameter_types!{ + pub const Six: u64 = 6; + } + + type RootOrSix = EnsureOneOf< + u64, + EnsureRoot, + EnsureSignedBy, + >; + + thread_local! { + pub static LAST_RANDOM: RefCell> = RefCell::new(None); + } + fn set_last_random(output: H256, known_since: u32) { + LAST_RANDOM.with(|p| *p.borrow_mut() = Some((output, known_since))) + } + pub struct TestPastRandomness; + impl Randomness for TestPastRandomness { + fn random(_subject: &[u8]) -> (H256, u32) { + LAST_RANDOM.with(|p| { + if let Some((output, known_since)) = &*p.borrow() { + (*output, *known_since) + } else { + (H256::zero(), frame_system::Module::::block_number()) + } + }) + } + } + + impl Config for Test { + type Event = Event; + type Leaser = TestLeaser; + type EndingPeriod = EndingPeriod; + type Randomness = TestPastRandomness; + type InitiateOrigin = RootOrSix; + type WeightInfo = crate::auctions::TestWeightInfo; + } + + // This function basically just builds a genesis storage key/value store according to + // our desired mock up. + pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig::{ + balances: vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)], + }.assimilate_storage(&mut t).unwrap(); + t.into() + } + + fn run_to_block(n: BlockNumber) { + while System::block_number() < n { + Auctions::on_finalize(System::block_number()); + Balances::on_finalize(System::block_number()); + System::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + Balances::on_initialize(System::block_number()); + Auctions::on_initialize(System::block_number()); + } + } + + #[test] + fn basic_setup_works() { + new_test_ext().execute_with(|| { + assert_eq!(AuctionCounter::get(), 0); + assert_eq!(TestLeaser::deposit_held(0u32.into(), &1), 0); + assert_eq!(Auctions::is_in_progress(), false); + assert_eq!(Auctions::is_ending(System::block_number()), None); + + run_to_block(10); + + assert_eq!(AuctionCounter::get(), 0); + assert_eq!(TestLeaser::deposit_held(0u32.into(), &1), 0); + assert_eq!(Auctions::is_in_progress(), false); + assert_eq!(Auctions::is_ending(System::block_number()), None); + }); + } + + #[test] + fn can_start_auction() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_noop!(Auctions::new_auction(Origin::signed(1), 5, 1), BadOrigin); + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + + assert_eq!(AuctionCounter::get(), 1); + assert_eq!(Auctions::is_in_progress(), true); + assert_eq!(Auctions::is_ending(System::block_number()), None); + }); + } + + #[test] + fn bidding_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 1, 1, 4, 5)); + + assert_eq!(Balances::reserved_balance(1), 5); + assert_eq!(Balances::free_balance(1), 5); + assert_eq!( + Auctions::winning(0).unwrap()[SlotRange::ZeroThree as u8 as usize], + Some((1, 0.into(), 5)) + ); + }); + } + + #[test] + fn under_bidding_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 1, 1, 4, 5)); + + assert_storage_noop!( + {assert_ok!(Auctions::bid(Origin::signed(2), 0.into(), 1, 1, 4, 1));} + ); + }); + } + + #[test] + fn over_bidding_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 1, 1, 4, 5)); + assert_ok!(Auctions::bid(Origin::signed(2), 0.into(), 1, 1, 4, 6)); + + assert_eq!(Balances::reserved_balance(1), 0); + assert_eq!(Balances::free_balance(1), 10); + assert_eq!(Balances::reserved_balance(2), 6); + assert_eq!(Balances::free_balance(2), 14); + assert_eq!( + Auctions::winning(0).unwrap()[SlotRange::ZeroThree as u8 as usize], + Some((2, 0.into(), 6)) + ); + }); + } + + #[test] + fn auction_proceeds_correctly() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + + assert_eq!(AuctionCounter::get(), 1); + assert_eq!(Auctions::is_in_progress(), true); + assert_eq!(Auctions::is_ending(System::block_number()), None); + + run_to_block(2); + assert_eq!(Auctions::is_in_progress(), true); + assert_eq!(Auctions::is_ending(System::block_number()), None); + + run_to_block(3); + assert_eq!(Auctions::is_in_progress(), true); + assert_eq!(Auctions::is_ending(System::block_number()), None); + + run_to_block(4); + assert_eq!(Auctions::is_in_progress(), true); + assert_eq!(Auctions::is_ending(System::block_number()), None); + + run_to_block(5); + assert_eq!(Auctions::is_in_progress(), true); + assert_eq!(Auctions::is_ending(System::block_number()), None); + + run_to_block(6); + assert_eq!(Auctions::is_in_progress(), true); + assert_eq!(Auctions::is_ending(System::block_number()), Some(0)); + + run_to_block(7); + assert_eq!(Auctions::is_in_progress(), true); + assert_eq!(Auctions::is_ending(System::block_number()), Some(1)); + + run_to_block(8); + assert_eq!(Auctions::is_in_progress(), true); + assert_eq!(Auctions::is_ending(System::block_number()), Some(2)); + + run_to_block(9); + assert_eq!(Auctions::is_in_progress(), false); + assert_eq!(Auctions::is_ending(System::block_number()), None); + }); + } + + #[test] + fn can_win_auction() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 1, 1, 4, 1)); + assert_eq!(Balances::reserved_balance(1), 1); + assert_eq!(Balances::free_balance(1), 9); + run_to_block(9); + + assert_eq!(leases(), vec![ + ((0.into(), 1), LeaseData { leaser: 1, amount: 1 }), + ((0.into(), 2), LeaseData { leaser: 1, amount: 1 }), + ((0.into(), 3), LeaseData { leaser: 1, amount: 1 }), + ((0.into(), 4), LeaseData { leaser: 1, amount: 1 }), + ]); + assert_eq!(TestLeaser::deposit_held(0.into(), &1), 1); + }); + } + + #[test] + fn can_win_auction_with_late_randomness() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 1, 1, 4, 1)); + assert_eq!(Balances::reserved_balance(1), 1); + assert_eq!(Balances::free_balance(1), 9); + run_to_block(8); + // Auction has not yet ended. + assert_eq!(leases(), vec![]); + assert!(Auctions::is_in_progress()); + // This will prevent the auction's winner from being decided in the next block, since the random + // seed was known before the final bids were made. + set_last_random(H256::zero(), 8); + // Auction definitely ended now, but we don't know exactly when in the last 3 blocks yet since + // no randomness available yet. + run_to_block(9); + // Auction has now ended... + assert!(!Auctions::is_in_progress()); + // ...But auction winner still not yet decided, so no leases yet. + assert_eq!(leases(), vec![]); + + // Random seed now updated to a value known at block 9, when the auction ended. This means + // that the winner can now be chosen. + set_last_random(H256::zero(), 9); + run_to_block(10); + // Auction ended and winner selected + assert!(!Auctions::is_in_progress()); + assert_eq!(leases(), vec![ + ((0.into(), 1), LeaseData { leaser: 1, amount: 1 }), + ((0.into(), 2), LeaseData { leaser: 1, amount: 1 }), + ((0.into(), 3), LeaseData { leaser: 1, amount: 1 }), + ((0.into(), 4), LeaseData { leaser: 1, amount: 1 }), + ]); + assert_eq!(TestLeaser::deposit_held(0.into(), &1), 1); + }); + } + + #[test] + fn can_win_incomplete_auction() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 1, 4, 4, 5)); + run_to_block(9); + + assert_eq!(leases(), vec![ + ((0.into(), 4), LeaseData { leaser: 1, amount: 5 }), + ]); + assert_eq!(TestLeaser::deposit_held(0.into(), &1), 5); + }); + } + + #[test] + fn should_choose_best_combination() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 1, 1, 1, 1)); + assert_ok!(Auctions::bid(Origin::signed(2), 0.into(), 1, 2, 3, 4)); + assert_ok!(Auctions::bid(Origin::signed(3), 0.into(), 1, 4, 4, 2)); + assert_ok!(Auctions::bid(Origin::signed(1), 1.into(), 1, 1, 4, 2)); + run_to_block(9); + + assert_eq!(leases(), vec![ + ((0.into(), 1), LeaseData { leaser: 1, amount: 1 }), + ((0.into(), 2), LeaseData { leaser: 2, amount: 4 }), + ((0.into(), 3), LeaseData { leaser: 2, amount: 4 }), + ((0.into(), 4), LeaseData { leaser: 3, amount: 2 }), + ]); + assert_eq!(TestLeaser::deposit_held(0.into(), &1), 1); + assert_eq!(TestLeaser::deposit_held(1.into(), &1), 0); + assert_eq!(TestLeaser::deposit_held(0.into(), &2), 4); + assert_eq!(TestLeaser::deposit_held(0.into(), &3), 2); + }); + } + + #[test] + fn independent_bids_should_fail() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(Auctions::new_auction(Origin::signed(6), 1, 1)); + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 1, 1, 2, 1)); + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 1, 2, 4, 1)); + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 1, 2, 2, 1)); + assert_noop!( + Auctions::bid(Origin::signed(1), 0.into(), 1, 3, 3, 1), + Error::::NonIntersectingRange + ); + }); + } + + #[test] + fn deposit_credit_should_work() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 1, 1, 1, 5)); + assert_eq!(Balances::reserved_balance(1), 5); + run_to_block(10); + + assert_eq!(leases(), vec![ + ((0.into(), 1), LeaseData { leaser: 1, amount: 5 }), + ]); + assert_eq!(TestLeaser::deposit_held(0.into(), &1), 5); + + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 2)); + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 2, 2, 2, 6)); + // Only 1 reserved since we have a deposit credit of 5. + assert_eq!(Balances::reserved_balance(1), 1); + run_to_block(20); + + assert_eq!(leases(), vec![ + ((0.into(), 1), LeaseData { leaser: 1, amount: 5 }), + ((0.into(), 2), LeaseData { leaser: 1, amount: 6 }), + ]); + assert_eq!(TestLeaser::deposit_held(0.into(), &1), 6); + }); + } + + #[test] + fn deposit_credit_on_alt_para_should_not_count() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + assert_ok!(Auctions::bid(Origin::signed(1), 0.into(), 1, 1, 1, 5)); + assert_eq!(Balances::reserved_balance(1), 5); + run_to_block(10); + + assert_eq!(leases(), vec![ + ((0.into(), 1), LeaseData { leaser: 1, amount: 5 }), + ]); + assert_eq!(TestLeaser::deposit_held(0.into(), &1), 5); + + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 2)); + assert_ok!(Auctions::bid(Origin::signed(1), 1.into(), 2, 2, 2, 6)); + // 6 reserved since we are bidding on a new para; only works because we don't + assert_eq!(Balances::reserved_balance(1), 6); + run_to_block(20); + + assert_eq!(leases(), vec![ + ((0.into(), 1), LeaseData { leaser: 1, amount: 5 }), + ((1.into(), 2), LeaseData { leaser: 1, amount: 6 }), + ]); + assert_eq!(TestLeaser::deposit_held(0.into(), &1), 5); + assert_eq!(TestLeaser::deposit_held(1.into(), &1), 6); + }); + } + + #[test] + fn multiple_bids_work_pre_ending() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + + for i in 1..6u64 { + run_to_block(i as _); + assert_ok!(Auctions::bid(Origin::signed(i), 0.into(), 1, 1, 4, i)); + for j in 1..6 { + assert_eq!(Balances::reserved_balance(j), if j == i { j } else { 0 }); + assert_eq!(Balances::free_balance(j), if j == i { j * 9 } else { j * 10 }); + } + } + + run_to_block(9); + assert_eq!(leases(), vec![ + ((0.into(), 1), LeaseData { leaser: 5, amount: 5 }), + ((0.into(), 2), LeaseData { leaser: 5, amount: 5 }), + ((0.into(), 3), LeaseData { leaser: 5, amount: 5 }), + ((0.into(), 4), LeaseData { leaser: 5, amount: 5 }), + ]); + }); + } + + #[test] + fn multiple_bids_work_post_ending() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_ok!(Auctions::new_auction(Origin::signed(6), 5, 1)); + + for i in 1..6u64 { + run_to_block((i + 3) as _); + assert_ok!(Auctions::bid(Origin::signed(i), 0.into(), 1, 1, 4, i)); + for j in 1..6 { + assert_eq!(Balances::reserved_balance(j), if j == i { j } else { 0 }); + assert_eq!(Balances::free_balance(j), if j == i { j * 9 } else { j * 10 }); + } + } + + run_to_block(9); + assert_eq!(leases(), vec![ + ((0.into(), 1), LeaseData { leaser: 3, amount: 3 }), + ((0.into(), 2), LeaseData { leaser: 3, amount: 3 }), + ((0.into(), 3), LeaseData { leaser: 3, amount: 3 }), + ((0.into(), 4), LeaseData { leaser: 3, amount: 3 }), + ]); + }); + } + + #[test] + fn incomplete_calculate_winners_works() { + let winning = [ + None, + None, + None, + None, + None, + None, + None, + None, + None, + Some((1, 0.into(), 1)), + ]; + let winners = vec![ + (1, 0.into(), 1, SlotRange::ThreeThree) + ]; + + assert_eq!(Auctions::calculate_winners(winning), winners); + } + + #[test] + fn first_incomplete_calculate_winners_works() { + let winning = [ + Some((1, 0.into(), 1)), + None, + None, + None, + None, + None, + None, + None, + None, + None, + ]; + let winners = vec![ + (1, 0.into(), 1, SlotRange::ZeroZero) + ]; + + assert_eq!(Auctions::calculate_winners(winning), winners); + } + + #[test] + fn calculate_winners_works() { + let mut winning = [ + /*0..0*/ + Some((2, 0.into(), 2)), + /*0..1*/ + None, + /*0..2*/ + None, + /*0..3*/ + Some((1, 100.into(), 1)), + /*1..1*/ + Some((3, 1.into(), 1)), + /*1..2*/ + None, + /*1..3*/ + None, + /*2..2*/ + Some((1, 2.into(), 53)), + /*2..3*/ + None, + /*3..3*/ + Some((5, 3.into(), 1)), + ]; + let winners = vec![ + (2, 0.into(), 2, SlotRange::ZeroZero), + (3, 1.into(), 1, SlotRange::OneOne), + (1, 2.into(), 53, SlotRange::TwoTwo), + (5, 3.into(), 1, SlotRange::ThreeThree), + ]; + assert_eq!(Auctions::calculate_winners(winning.clone()), winners); + + winning[SlotRange::ZeroOne as u8 as usize] = Some((4, 10.into(), 3)); + let winners = vec![ + (4, 10.into(), 3, SlotRange::ZeroOne), + (1, 2.into(), 53, SlotRange::TwoTwo), + (5, 3.into(), 1, SlotRange::ThreeThree), + ]; + assert_eq!(Auctions::calculate_winners(winning.clone()), winners); + + winning[SlotRange::ZeroThree as u8 as usize] = Some((1, 100.into(), 100)); + let winners = vec![ + (1, 100.into(), 100, SlotRange::ZeroThree), + ]; + assert_eq!(Auctions::calculate_winners(winning.clone()), winners); + } + + #[test] + fn lower_bids_are_correctly_refunded() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(Auctions::new_auction(Origin::signed(6), 1, 1)); + let para_1 = ParaId::from(1); + let para_2 = ParaId::from(2); + + // Make a bid and reserve a balance + assert_ok!(Auctions::bid(Origin::signed(1), para_1, 1, 1, 4, 10)); + assert_eq!(Balances::reserved_balance(1), 10); + assert_eq!(ReservedAmounts::::get((1, para_1)), Some(10)); + assert_eq!(Balances::reserved_balance(2), 0); + assert_eq!(ReservedAmounts::::get((2, para_2)), None); + + // Bigger bid, reserves new balance and returns funds + assert_ok!(Auctions::bid(Origin::signed(2), para_2, 1, 1, 4, 20)); + assert_eq!(Balances::reserved_balance(1), 0); + assert_eq!(ReservedAmounts::::get((1, para_1)), None); + assert_eq!(Balances::reserved_balance(2), 20); + assert_eq!(ReservedAmounts::::get((2, para_2)), Some(20)); + }); + } + + #[test] + fn initialize_winners_in_ending_period_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(Auctions::new_auction(Origin::signed(6), 9, 1)); + let para_1 = ParaId::from(1); + let para_2 = ParaId::from(2); + let para_3 = ParaId::from(3); + + // Make bids + assert_ok!(Auctions::bid(Origin::signed(1), para_1, 1, 1, 4, 10)); + assert_ok!(Auctions::bid(Origin::signed(2), para_2, 1, 3, 4, 20)); + + assert_eq!(Auctions::is_ending(System::block_number()), None); + assert_eq!(Auctions::winning(0), Some([ + None, + None, + None, + Some((1, para_1, 10)), + None, + None, + None, + None, + Some((2, para_2, 20)), + None, + ])); + + run_to_block(9); + assert_eq!(Auctions::is_ending(System::block_number()), None); + + run_to_block(10); + assert_eq!(Auctions::is_ending(System::block_number()), Some(0)); + assert_eq!(Auctions::winning(0), Some([ + None, + None, + None, + Some((1, para_1, 10)), + None, + None, + None, + None, + Some((2, para_2, 20)), + None, + ])); + + run_to_block(11); + assert_eq!(Auctions::is_ending(System::block_number()), Some(1)); + assert_eq!(Auctions::winning(1), Some([ + None, + None, + None, + Some((1, para_1, 10)), + None, + None, + None, + None, + Some((2, para_2, 20)), + None, + ])); + assert_ok!(Auctions::bid(Origin::signed(3), para_3, 1, 3, 4, 30)); + + run_to_block(12); + assert_eq!(Auctions::is_ending(System::block_number()), Some(2)); + assert_eq!(Auctions::winning(2), Some([ + None, + None, + None, + Some((1, para_1, 10)), + None, + None, + None, + None, + Some((3, para_3, 30)), + None, + ])); + }); + } +} + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking { + use super::{*, Module as Auctions}; + use frame_system::RawOrigin; + use frame_support::traits::OnInitialize; + use sp_runtime::traits::Bounded; + + use frame_benchmarking::{benchmarks, whitelisted_caller, account, impl_benchmark_test_suite}; + + fn assert_last_event(generic_event: ::Event) { + let events = frame_system::Module::::events(); + let system_event: ::Event = generic_event.into(); + // compare to the last event record + let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); + } + + benchmarks! { + where_clause { where T: pallet_babe::Config } + + new_auction { + let duration = T::BlockNumber::max_value(); + let lease_period_index = LeasePeriodOf::::max_value(); + let origin = T::InitiateOrigin::successful_origin(); + }: _(RawOrigin::Root, duration, lease_period_index) + verify { + assert_last_event::(RawEvent::AuctionStarted( + AuctionCounter::get(), + LeasePeriodOf::::max_value(), + T::BlockNumber::max_value(), + ).into()); + } + + // Worst case scenario a new bid comes in which kicks out an existing bid for the same slot. + bid { + // Create a new auction + let duration = T::BlockNumber::max_value(); + let lease_period_index = LeasePeriodOf::::zero(); + Auctions::::new_auction(RawOrigin::Root.into(), duration, lease_period_index)?; + + // Make an existing bid + let para = ParaId::from(0); + let auction_index = AuctionCounter::get(); + let first_slot = AuctionInfo::::get().unwrap().0; + let last_slot = first_slot + 3u32.into(); + let first_amount = CurrencyOf::::minimum_balance(); + let first_bidder: T::AccountId = account("first_bidder", 0, 0); + CurrencyOf::::make_free_balance_be(&first_bidder, BalanceOf::::max_value()); + Auctions::::bid( + RawOrigin::Signed(first_bidder.clone()).into(), + para, + auction_index, + first_slot, + last_slot, + first_amount, + )?; + + let caller: T::AccountId = whitelisted_caller(); + CurrencyOf::::make_free_balance_be(&caller, BalanceOf::::max_value()); + let new_para = ParaId::from(1); + let bigger_amount = CurrencyOf::::minimum_balance().saturating_mul(10u32.into()); + assert_eq!(CurrencyOf::::reserved_balance(&first_bidder), first_amount); + }: _(RawOrigin::Signed(caller.clone()), new_para, auction_index, first_slot, last_slot, bigger_amount) + verify { + // Confirms that we unreserved funds from a previous bidder, which is worst case scenario. + assert_eq!(CurrencyOf::::reserved_balance(&caller), bigger_amount); + } + + // Worst case: 10 bidders taking all wining spots, and we need to calculate the winner for auction end. + on_initialize { + // Create a new auction + let duration: T::BlockNumber = 99u32.into(); + let lease_period_index = LeasePeriodOf::::zero(); + let now = frame_system::Module::::block_number(); + Auctions::::new_auction(RawOrigin::Root.into(), duration, lease_period_index)?; + let auction_index = AuctionCounter::get(); + + let minimum_balance = CurrencyOf::::minimum_balance(); + + for n in 1 ..= SLOT_RANGE_COUNT as u32 { + let bidder = account("bidder", n, 0); + CurrencyOf::::make_free_balance_be(&bidder, BalanceOf::::max_value()); + + let (start, end) = match n { + 1 => (0u32, 0u32), + 2 => (0, 1), + 3 => (0, 2), + 4 => (0, 3), + 5 => (1, 1), + 6 => (1, 2), + 7 => (1, 3), + 8 => (2, 2), + 9 => (2, 3), + 10 => (3, 3), + _ => panic!("test not meant for this"), + }; + + Auctions::::bid( + RawOrigin::Signed(bidder).into(), + ParaId::from(n), + auction_index, + lease_period_index + start.into(), // First Slot + lease_period_index + end.into(), // Last slot + minimum_balance.saturating_mul(n.into()), // Amount + )?; + } + + for winner in Winning::::get(T::BlockNumber::from(0u32)).unwrap().iter() { + assert!(winner.is_some()); + } + + // Move ahead to the block we want to initialize + frame_system::Module::::set_block_number(duration + now + T::EndingPeriod::get()); + + // Trigger epoch change for new random number value: + { + pallet_babe::Module::::on_initialize(duration + now + T::EndingPeriod::get()); + let authorities = pallet_babe::Module::::authorities(); + let next_authorities = authorities.clone(); + pallet_babe::Module::::enact_epoch_change(authorities, next_authorities); + } + + }: { + Auctions::::on_initialize(duration + now + T::EndingPeriod::get()); + } verify { + assert_last_event::(RawEvent::AuctionClosed(auction_index).into()); + } + } + + impl_benchmark_test_suite!( + Auctions, + crate::integration_tests::new_test_ext(), + crate::integration_tests::Test, + ); +} diff --git a/runtime/common/src/crowdloan.rs b/runtime/common/src/crowdloan.rs index b68c51aa67c2..9b6c664ca707 100644 --- a/runtime/common/src/crowdloan.rs +++ b/runtime/common/src/crowdloan.rs @@ -70,34 +70,55 @@ use frame_support::{ decl_module, decl_storage, decl_event, decl_error, ensure, storage::child, traits::{ - Currency, Get, OnUnbalanced, ExistenceRequirement::AllowDeath + Currency, ReservableCurrency, Get, OnUnbalanced, ExistenceRequirement::AllowDeath }, + pallet_prelude::{Weight, DispatchResultWithPostInfo}, }; use frame_system::ensure_signed; use sp_runtime::{ - ModuleId, DispatchResult, MultiSignature, MultiSigner, + ModuleId, DispatchResult, RuntimeDebug, MultiSignature, MultiSigner, traits::{ AccountIdConversion, Hash, Saturating, Zero, CheckedAdd, Bounded, Verify, IdentifyAccount, }, }; -use crate::slots; +use crate::traits::{Registrar, Auctioneer}; use parity_scale_codec::{Encode, Decode}; use sp_std::vec::Vec; -use primitives::v1::{Id as ParaId, HeadData}; +use primitives::v1::Id as ParaId; + +type CurrencyOf = <::Auctioneer as Auctioneer>::Currency; +type LeasePeriodOf = <::Auctioneer as Auctioneer>::LeasePeriod; +type BalanceOf = as Currency<::AccountId>>::Balance; -pub type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; #[allow(dead_code)] -pub type NegativeImbalanceOf = - <::Currency as Currency<::AccountId>>::NegativeImbalance; +type NegativeImbalanceOf = as Currency<::AccountId>>::NegativeImbalance; + +type TrieIndex = u32; + +pub trait WeightInfo { + fn create() -> Weight; + fn contribute() -> Weight; + fn withdraw() -> Weight; + fn dissolve(k: u32, ) -> Weight; + fn on_initialize(n: u32, ) -> Weight; +} -pub trait Config: slots::Config { +pub struct TestWeightInfo; +impl WeightInfo for TestWeightInfo { + fn create() -> Weight { 0 } + fn contribute() -> Weight { 0 } + fn withdraw() -> Weight { 0 } + fn dissolve(_k: u32, ) -> Weight { 0 } + fn on_initialize(_n: u32, ) -> Weight { 0 } +} + +pub trait Config: frame_system::Config { type Event: From> + Into<::Event>; /// ModuleID for the crowdloan module. An appropriate value could be ```ModuleId(*b"py/cfund")``` type ModuleId: Get; - /// The amount to be held on deposit by the owner of a crowdloan. + /// The amount to be held on deposit by the depositor of a crowdloan. type SubmissionDeposit: Get>; /// The minimum amount that may be contributed into a crowdloan. Should almost certainly be at @@ -113,36 +134,39 @@ pub trait Config: slots::Config { /// Max number of storage keys to remove per extrinsic call. type RemoveKeysLimit: Get; -} -/// Simple index for identifying a fund. -pub type FundIndex = u32; + /// The parachain registrar type. We jus use this to ensure that only the manager of a para is able to + /// start a crowdloan for its slot. + type Registrar: Registrar; + + /// The type representing the auctioning system. + type Auctioneer: Auctioneer< + AccountId=Self::AccountId, + BlockNumber=Self::BlockNumber, + LeasePeriod=Self::BlockNumber, + >; -#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "std", derive(Debug))] + /// Weight Information for the Extrinsics in the Pallet + type WeightInfo: WeightInfo; +} + +#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug)] pub enum LastContribution { Never, - PreEnding(slots::AuctionIndex), + PreEnding(u32), Ending(BlockNumber), } -#[derive(Encode, Decode, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "std", derive(Debug))] -struct DeployData { - code_hash: Hash, - code_size: u32, - initial_head_data: HeadData, -} - -#[derive(Encode, Decode, Clone, PartialEq, Eq)] -#[cfg_attr(feature = "std", derive(Debug))] +/// Information on a funding effort for a pre-existing parachain. We assume that the parachain ID +/// is known as it's used for the key of the storage item for which this is the value (`Funds`). +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)] #[codec(dumb_trait_bound)] -pub struct FundInfo { - /// The parachain that this fund has funded, if there is one. As long as this is `Some`, then - /// the funds may not be withdrawn and the fund cannot be dissolved. - parachain: Option, +pub struct FundInfo { + /// True if the fund is being retired. This can only be set once and only when the current + /// lease period is greater than the `last_slot`. + retiring: bool, /// The owning account who placed the deposit. - owner: AccountId, + depositor: AccountId, /// An optional verifier. If exists, contributions must be signed by verifier. verifier: Option, /// The amount of deposit placed. @@ -163,31 +187,30 @@ pub struct FundInfo { last_contribution: LastContribution, /// First slot in range to bid on; it's actually a LeasePeriod, but that's the same type as /// BlockNumber. - first_slot: BlockNumber, + first_slot: LeasePeriod, /// Last slot in range to bid on; it's actually a LeasePeriod, but that's the same type as /// BlockNumber. - last_slot: BlockNumber, - /// The deployment data associated with this fund, if any. Once set it may not be reset. First - /// is the code hash, second is the code size, third is the initial head data. - deploy_data: Option>, + last_slot: LeasePeriod, + /// Index used for the child trie of this fund + trie_index: TrieIndex, } decl_storage! { trait Store for Module as Crowdloan { /// Info on all of the funds. Funds get(fn funds): - map hasher(twox_64_concat) FundIndex - => Option, T::Hash, T::BlockNumber>>; - - /// The total number of funds that have so far been allocated. - FundCount get(fn fund_count): FundIndex; + map hasher(twox_64_concat) ParaId + => Option, T::BlockNumber, LeasePeriodOf>>; /// The funds that have had additional contributions during the last block. This is used /// in order to determine which funds should submit new or updated bids. - NewRaise get(fn new_raise): Vec; + NewRaise get(fn new_raise): Vec; /// The number of auctions that have entered into their ending period so far. - EndingsCount get(fn endings_count): slots::AuctionIndex; + EndingsCount get(fn endings_count): u32; + + /// Tracker for the next available trie index + NextTrieIndex get(fn next_trie_index): u32; } } @@ -197,29 +220,31 @@ decl_event! { Balance = BalanceOf, { /// Create a new crowdloaning campaign. [fund_index] - Created(FundIndex), + Created(ParaId), /// Contributed to a crowd sale. [who, fund_index, amount] - Contributed(AccountId, FundIndex, Balance), + Contributed(AccountId, ParaId, Balance), /// Withdrew full balance of a contributor. [who, fund_index, amount] - Withdrew(AccountId, FundIndex, Balance), + Withdrew(AccountId, ParaId, Balance), /// Fund is placed into retirement. [fund_index] - Retiring(FundIndex), + Retiring(ParaId), /// Fund is partially dissolved, i.e. there are some left over child /// keys that still need to be killed. [fund_index] - PartiallyDissolved(FundIndex), + PartiallyDissolved(ParaId), /// Fund is dissolved. [fund_index] - Dissolved(FundIndex), - /// The deploy data of the funded parachain is setted. [fund_index] - DeployDataFixed(FundIndex), - /// Onboarding process for a winning parachain fund is completed. [find_index, parachain_id] - Onboarded(FundIndex, ParaId), + Dissolved(ParaId), + /// The deploy data of the funded parachain is set. [fund_index] + DeployDataFixed(ParaId), + /// On-boarding process for a winning parachain fund is completed. [find_index, parachain_id] + Onboarded(ParaId, ParaId), /// The result of trying to submit a new bid to the Slots pallet. - HandleBidResult(FundIndex, DispatchResult), + HandleBidResult(ParaId, DispatchResult), } } decl_error! { pub enum Error for Module { + /// The first slot needs to at least be less than 3 `max_value`. + FirstSlotTooFarInFuture, /// Last slot must be greater than first slot. LastSlotBeforeFirstSlot, /// The last slot cannot be more then 3 slots after the first slot. @@ -231,24 +256,19 @@ decl_error! { /// The contribution was below the minimum, `MinContribution`. ContributionTooSmall, /// Invalid fund index. - InvalidFundIndex, + InvalidParaId, /// Contributions exceed maximum amount. CapExceeded, /// The contribution period has already ended. ContributionPeriodOver, /// The origin of this call is invalid. InvalidOrigin, - /// Deployment data for a fund can only be set once. The deployment data for this fund - /// already exists. - ExistingDeployData, - /// Deployment data has not been set for this fund. - UnsetDeployData, - /// This fund has already been onboarded. - AlreadyOnboard, /// This crowdloan does not correspond to a parachain. NotParachain, - /// This parachain still has its deposit. Implies that it has already been offboarded. - ParaHasDeposit, + /// This parachain lease is still active and retirement cannot yet begin. + LeaseActive, + /// This parachain's bid or lease is still active and withdraw cannot yet begin. + BidOrLeaseActive, /// Funds have not yet been returned. FundsNotReturned, /// Fund has not yet retired. @@ -259,46 +279,57 @@ decl_error! { NoContributions, /// This crowdloan has an active parachain and cannot be dissolved. HasActiveParachain, - /// The retirement period has not ended. - InRetirementPeriod, + /// The crowdloan is not ready to dissolve. Potentially still has a slot or in retirement period. + NotReadyToDissolve, /// Invalid signature. InvalidSignature, } } decl_module! { - pub struct Module for enum Call where origin: T::Origin { + pub struct Module for enum Call where origin: ::Origin { type Error = Error; const ModuleId: ModuleId = T::ModuleId::get(); + const MinContribution: BalanceOf = T::MinContribution::get(); + const RemoveKeysLimit: u32 = T::RemoveKeysLimit::get(); + const RetirementPeriod: T::BlockNumber = T::RetirementPeriod::get(); fn deposit_event() = default; /// Create a new crowdloaning campaign for a parachain slot deposit for the current auction. - #[weight = 100_000_000] - fn create(origin, + #[weight = T::WeightInfo::create()] + pub fn create(origin, + #[compact] index: ParaId, #[compact] cap: BalanceOf, - #[compact] first_slot: T::BlockNumber, - #[compact] last_slot: T::BlockNumber, + #[compact] first_slot: LeasePeriodOf, + #[compact] last_slot: LeasePeriodOf, #[compact] end: T::BlockNumber, verifier: Option, ) { - let owner = ensure_signed(origin)?; + let depositor = ensure_signed(origin)?; - ensure!(first_slot < last_slot, Error::::LastSlotBeforeFirstSlot); - ensure!(last_slot <= first_slot + 3u32.into(), Error::::LastSlotTooFarInFuture); + ensure!(first_slot <= last_slot, Error::::LastSlotBeforeFirstSlot); + let last_slot_limit = first_slot.checked_add(&3u32.into()).ok_or(Error::::FirstSlotTooFarInFuture)?; + ensure!(last_slot <= last_slot_limit, Error::::LastSlotTooFarInFuture); ensure!(end > >::block_number(), Error::::CannotEndInPast); - let index = FundCount::get(); - let next_index = index.checked_add(1).ok_or(Error::::Overflow)?; + // There should not be an existing fund. + ensure!(!Funds::::contains_key(index), Error::::FundNotEnded); + + let manager = T::Registrar::manager_of(index).ok_or(Error::::InvalidParaId)?; + ensure!(depositor == manager, Error::::InvalidOrigin); + + let trie_index = Self::next_trie_index(); + let new_trie_index = trie_index.checked_add(1).ok_or(Error::::Overflow)?; let deposit = T::SubmissionDeposit::get(); - T::Currency::transfer(&owner, &Self::fund_account_id(index), deposit, AllowDeath)?; - FundCount::put(next_index); - >::insert(index, FundInfo { - parachain: None, - owner, + CurrencyOf::::reserve(&depositor, deposit)?; + + Funds::::insert(index, FundInfo { + retiring: false, + depositor, verifier, deposit, raised: Zero::zero(), @@ -307,35 +338,35 @@ decl_module! { last_contribution: LastContribution::Never, first_slot, last_slot, - deploy_data: None, + trie_index, }); + NextTrieIndex::put(new_trie_index); + Self::deposit_event(RawEvent::Created(index)); } /// Contribute to a crowd sale. This will transfer some balance over to fund a parachain /// slot. It will be withdrawable in two instances: the parachain becomes retired; or the /// slot is unable to be purchased and the timeout expires. - /// A valid signature maybe required in order to accept the contribution. - #[weight = 0] - fn contribute( - origin, - #[compact] index: FundIndex, + #[weight = T::WeightInfo::contribute()] + pub fn contribute(origin, + #[compact] index: ParaId, #[compact] value: BalanceOf, signature: Option ) { let who = ensure_signed(origin)?; ensure!(value >= T::MinContribution::get(), Error::::ContributionTooSmall); - let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; + let mut fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; fund.raised = fund.raised.checked_add(&value).ok_or(Error::::Overflow)?; ensure!(fund.raised <= fund.cap, Error::::CapExceeded); // Make sure crowdloan has not ended let now = >::block_number(); - ensure!(fund.end > now, Error::::ContributionPeriodOver); + ensure!(now < fund.end, Error::::ContributionPeriodOver); - let old_balance = Self::contribution_get(index, &who); + let old_balance = Self::contribution_get(fund.trie_index, &who); if let Some(ref verifier) = fund.verifier { let signature = signature.ok_or(Error::::InvalidSignature)?; @@ -344,12 +375,12 @@ decl_module! { ensure!(valid, Error::::InvalidSignature); } - T::Currency::transfer(&who, &Self::fund_account_id(index), value, AllowDeath)?; + CurrencyOf::::transfer(&who, &Self::fund_account_id(index), value, AllowDeath)?; let balance = old_balance.saturating_add(value); - Self::contribution_put(index, &who, &balance); + Self::contribution_put(fund.trie_index, &who, &balance); - if >::is_ending(now).is_some() { + if T::Auctioneer::is_ending(now).is_some() { match fund.last_contribution { // In ending period; must ensure that we are in NewRaise. LastContribution::Ending(n) if n == now => { @@ -377,188 +408,121 @@ decl_module! { } } - >::insert(index, &fund); + Funds::::insert(index, &fund); Self::deposit_event(RawEvent::Contributed(who, index, value)); } - /// Set the deploy data of the funded parachain if not already set. Once set, this cannot - /// be changed again. + /// Withdraw full balance of a contributor. /// - /// - `origin` must be the fund owner. - /// - `index` is the fund index that `origin` owns and whose deploy data will be set. - /// - `code_hash` is the hash of the parachain's Wasm validation function. - /// - `initial_head_data` is the parachain's initial head data. - #[weight = 0] - fn fix_deploy_data(origin, - #[compact] index: FundIndex, - code_hash: T::Hash, - code_size: u32, - initial_head_data: HeadData, - ) { - let who = ensure_signed(origin)?; - - let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; - ensure!(fund.owner == who, Error::::InvalidOrigin); // must be fund owner - ensure!(fund.deploy_data.is_none(), Error::::ExistingDeployData); - - fund.deploy_data = Some(DeployData { code_hash, code_size, initial_head_data }); - - >::insert(index, &fund); - - Self::deposit_event(RawEvent::DeployDataFixed(index)); - } - - /// Complete onboarding process for a winning parachain fund. This can be called once by - /// any origin once a fund wins a slot and the fund has set its deploy data (using - /// `fix_deploy_data`). + /// Origin must be signed. /// - /// - `index` is the fund index that `origin` owns and whose deploy data will be set. - /// - `para_id` is the parachain index that this fund won. - #[weight = 0] - fn onboard(origin, - #[compact] index: FundIndex, - #[compact] para_id: ParaId - ) { - ensure_signed(origin)?; - - let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; - let DeployData { code_hash, code_size, initial_head_data } - = fund.clone().deploy_data.ok_or(Error::::UnsetDeployData)?; - ensure!(fund.parachain.is_none(), Error::::AlreadyOnboard); - fund.parachain = Some(para_id); - - let fund_origin = frame_system::RawOrigin::Signed(Self::fund_account_id(index)).into(); - >::fix_deploy_data( - fund_origin, - index, - para_id, - code_hash, - code_size, - initial_head_data, - )?; - - >::insert(index, &fund); - - Self::deposit_event(RawEvent::Onboarded(index, para_id)); - } - - /// Note that a successful fund has lost its parachain slot, and place it into retirement. - #[weight = 0] - fn begin_retirement(origin, #[compact] index: FundIndex) { + /// The fund must be either in, or ready for, retirement. For a fund to be *in* retirement, then the retirement + /// flag must be set. For a fund to be ready for retirement, then: + /// - it must not already be in retirement; + /// - the amount of raised funds must be bigger than the _free_ balance of the account; + /// - and either: + /// - the block number must be at least `end`; or + /// - the current lease period must be greater than the fund's `last_slot`. + /// + /// In this case, the fund's retirement flag is set and its `end` is reset to the current block + /// number. + /// + /// - `who`: The account whose contribution should be withdrawn. + /// - `index`: The parachain to whose crowdloan the contribution was made. + #[weight = T::WeightInfo::withdraw()] + pub fn withdraw(origin, who: T::AccountId, #[compact] index: ParaId) { ensure_signed(origin)?; - let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; - let parachain_id = fund.parachain.take().ok_or(Error::::NotParachain)?; - // No deposit information implies the parachain was off-boarded - ensure!(>::deposits(parachain_id).len() == 0, Error::::ParaHasDeposit); - let account = Self::fund_account_id(index); - // Funds should be returned at the end of off-boarding - ensure!(T::Currency::free_balance(&account) >= fund.raised, Error::::FundsNotReturned); - - // This fund just ended. Withdrawal period begins. - let now = >::block_number(); - fund.end = now; - - >::insert(index, &fund); - - Self::deposit_event(RawEvent::Retiring(index)); - } - - /// Withdraw full balance of a contributor to an unsuccessful or off-boarded fund. - #[weight = 0] - fn withdraw(origin, who: T::AccountId, #[compact] index: FundIndex) { - let _ = ensure_signed(origin)?; - - let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; - ensure!(fund.parachain.is_none(), Error::::FundNotRetired); - let now = >::block_number(); + let mut fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; // `fund.end` can represent the end of a failed crowdsale or the beginning of retirement - ensure!(now >= fund.end, Error::::FundNotEnded); + let now = frame_system::Module::::block_number(); + let current_lease_period = T::Auctioneer::lease_period_index(); + ensure!(now >= fund.end || current_lease_period > fund.last_slot, Error::::FundNotEnded); + + let fund_account = Self::fund_account_id(index); + // free balance must equal amount raised, otherwise a bid or lease must be active. + ensure!(CurrencyOf::::free_balance(&fund_account) == fund.raised, Error::::BidOrLeaseActive); - let balance = Self::contribution_get(index, &who); + let balance = Self::contribution_get(fund.trie_index, &who); ensure!(balance > Zero::zero(), Error::::NoContributions); // Avoid using transfer to ensure we don't pay any fees. - let fund_account = Self::fund_account_id(index); - T::Currency::transfer(&fund_account, &who, balance, AllowDeath)?; + CurrencyOf::::transfer(&fund_account, &who, balance, AllowDeath)?; - Self::contribution_kill(index, &who); + Self::contribution_kill(fund.trie_index, &who); fund.raised = fund.raised.saturating_sub(balance); + if !fund.retiring { + fund.retiring = true; + fund.end = now; + } - >::insert(index, &fund); + Funds::::insert(index, &fund); Self::deposit_event(RawEvent::Withdrew(who, index, balance)); } - /// Remove a fund after either: it was unsuccessful and it timed out; or it was successful - /// but it has been retired from its parachain slot. This places any deposits that were not - /// withdrawn into the treasury. - #[weight = 0] - fn dissolve(origin, #[compact] index: FundIndex) { + /// Remove a fund after the retirement period has ended. + /// + /// This places any deposits that were not withdrawn into the treasury. + #[weight = T::WeightInfo::dissolve(T::RemoveKeysLimit::get())] + pub fn dissolve(origin, #[compact] index: ParaId) -> DispatchResultWithPostInfo { ensure_signed(origin)?; - let fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; - ensure!(fund.parachain.is_none(), Error::::HasActiveParachain); - let now = >::block_number(); - ensure!( - now >= fund.end.saturating_add(T::RetirementPeriod::get()), - Error::::InRetirementPeriod - ); + let fund = Self::funds(index).ok_or(Error::::InvalidParaId)?; + let now = frame_system::Module::::block_number(); + let dissolution = fund.end.saturating_add(T::RetirementPeriod::get()); + ensure!((fund.retiring && now >= dissolution) || fund.raised.is_zero(), Error::::NotReadyToDissolve); // Try killing the crowdloan child trie - match Self::crowdloan_kill(index) { - // TODO use this value for refund - child::KillChildStorageResult::AllRemoved(_) => { - let account = Self::fund_account_id(index); - T::Currency::transfer(&account, &fund.owner, fund.deposit, AllowDeath)?; + match Self::crowdloan_kill(fund.trie_index) { + child::KillChildStorageResult::AllRemoved(num_removed) => { + CurrencyOf::::unreserve(&fund.depositor, fund.deposit); // Remove all other balance from the account into orphaned funds. - let (imbalance, _) = T::Currency::slash(&account, BalanceOf::::max_value()); + let account = Self::fund_account_id(index); + let (imbalance, _) = CurrencyOf::::slash(&account, BalanceOf::::max_value()); T::OrphanedFunds::on_unbalanced(imbalance); - >::remove(index); + Funds::::remove(index); Self::deposit_event(RawEvent::Dissolved(index)); + + Ok(Some(T::WeightInfo::dissolve(num_removed)).into()) }, - // TODO use this value for refund - child::KillChildStorageResult::SomeRemaining(_) => { + child::KillChildStorageResult::SomeRemaining(num_removed) => { Self::deposit_event(RawEvent::PartiallyDissolved(index)); + Ok(Some(T::WeightInfo::dissolve(num_removed)).into()) } } } fn on_initialize(n: T::BlockNumber) -> frame_support::weights::Weight { - if let Some(n) = >::is_ending(n) { - let auction_index = >::auction_counter(); + if let Some(n) = T::Auctioneer::is_ending(n) { if n.is_zero() { // first block of ending period. EndingsCount::mutate(|c| *c += 1); } - for (fund, index) in NewRaise::take().into_iter().filter_map(|i| Self::funds(i).map(|f| (f, i))) { - let bidder = slots::Bidder::New(slots::NewBidder { - who: Self::fund_account_id(index), - /// FundIndex and slots::SubId happen to be the same type (u32). If this - /// ever changes, then some sort of conversion will be needed here. - sub: index, - }); - + let new_raise = NewRaise::take(); + let new_raise_len = new_raise.len() as u32; + for (fund, para_id) in new_raise.into_iter().filter_map(|i| Self::funds(i).map(|f| (f, i))) { // Care needs to be taken by the crowdloan creator that this function will succeed given // the crowdloaning configuration. We do some checks ahead of time in crowdloan `create`. - let result = >::handle_bid( - bidder, - auction_index, + let result = T::Auctioneer::place_bid( + Self::fund_account_id(para_id), + para_id, fund.first_slot, fund.last_slot, fund.raised, ); - Self::deposit_event(RawEvent::HandleBidResult(index, result)); + Self::deposit_event(RawEvent::HandleBidResult(para_id, result)); } + T::WeightInfo::on_initialize(new_raise_len) + } else { + T::DbWeight::get().reads(1) } - - 0 } } } @@ -568,37 +532,47 @@ impl Module { /// /// This actually does computation. If you need to keep using it, then make sure you cache the /// value and only call this once. - pub fn fund_account_id(index: FundIndex) -> T::AccountId { + pub fn fund_account_id(index: ParaId) -> T::AccountId { T::ModuleId::get().into_sub_account(index) } - pub fn id_from_index(index: FundIndex) -> child::ChildInfo { + pub fn id_from_index(index: TrieIndex) -> child::ChildInfo { let mut buf = Vec::new(); buf.extend_from_slice(b"crowdloan"); - buf.extend_from_slice(&index.to_le_bytes()[..]); + buf.extend_from_slice(&index.encode()[..]); child::ChildInfo::new_default(T::Hashing::hash(&buf[..]).as_ref()) } - pub fn contribution_put(index: FundIndex, who: &T::AccountId, balance: &BalanceOf) { + pub fn contribution_put(index: TrieIndex, who: &T::AccountId, balance: &BalanceOf) { who.using_encoded(|b| child::put(&Self::id_from_index(index), b, balance)); } - pub fn contribution_get(index: FundIndex, who: &T::AccountId) -> BalanceOf { + pub fn contribution_get(index: TrieIndex, who: &T::AccountId) -> BalanceOf { who.using_encoded(|b| child::get_or_default::>( &Self::id_from_index(index), b, )) } - pub fn contribution_kill(index: FundIndex, who: &T::AccountId) { + pub fn contribution_kill(index: TrieIndex, who: &T::AccountId) { who.using_encoded(|b| child::kill(&Self::id_from_index(index), b)); } - pub fn crowdloan_kill(index: FundIndex) -> child::KillChildStorageResult { + pub fn crowdloan_kill(index: TrieIndex) -> child::KillChildStorageResult { child::kill_storage(&Self::id_from_index(index), Some(T::RemoveKeysLimit::get())) } } +impl crate::traits::OnSwap for Module { + fn on_swap(one: ParaId, other: ParaId) { + Funds::::mutate(one, |x| + Funds::::mutate(other, |y| + sp_std::mem::swap(x, y) + ) + ) + } +} + #[cfg(any(feature = "runtime-benchmarks", test))] mod crypto { use sp_core::ed25519; @@ -624,22 +598,24 @@ mod crypto { mod tests { use super::*; - use std::{collections::HashMap, cell::RefCell, sync::Arc}; + use std::{cell::RefCell, sync::Arc}; use frame_support::{ assert_ok, assert_noop, parameter_types, traits::{OnInitialize, OnFinalize}, }; use sp_core::H256; - use primitives::v1::{Id as ParaId, ValidationCode}; + use primitives::v1::Id as ParaId; // The testing primitives are very useful for avoiding having to work with signatures // or public keys. `u64` is used as the `AccountId` and no `Signature`s are requried. use sp_runtime::{ - Permill, testing::Header, - traits::{BlakeTwo256, IdentityLookup}, + testing::Header, traits::{BlakeTwo256, IdentityLookup}, + }; + use crate::{ + mock::TestRegistrar, + traits::OnSwap, + crowdloan, }; use sp_keystore::{KeystoreExt, testing::KeyStore}; - use crate::slots::{self, Registrar}; - use crate::crowdloan; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -652,10 +628,7 @@ mod tests { { System: frame_system::{Module, Call, Config, Storage, Event}, Balances: pallet_balances::{Module, Call, Storage, Config, Event}, - Treasury: pallet_treasury::{Module, Call, Storage, Config, Event}, - Slots: slots::{Module, Call, Storage, Event}, Crowdloan: crowdloan::{Module, Call, Storage, Event}, - RandomnessCollectiveFlip: pallet_randomness_collective_flip::{Module, Call, Storage}, } ); @@ -687,9 +660,11 @@ mod tests { type SystemWeightInfo = (); type SS58Prefix = (); } + parameter_types! { pub const ExistentialDeposit: u64 = 1; } + impl pallet_balances::Config for Test { type Balance = u64; type Event = Event; @@ -700,93 +675,77 @@ mod tests { type WeightInfo = (); } - parameter_types! { - pub const ProposalBond: Permill = Permill::from_percent(5); - pub const ProposalBondMinimum: u64 = 1; - pub const SpendPeriod: u64 = 2; - pub const Burn: Permill = Permill::from_percent(50); - pub const TreasuryModuleId: ModuleId = ModuleId(*b"py/trsry"); - } - impl pallet_treasury::Config for Test { - type Currency = pallet_balances::Module; - type ApproveOrigin = frame_system::EnsureRoot; - type RejectOrigin = frame_system::EnsureRoot; - type Event = Event; - type OnSlash = (); - type ProposalBond = ProposalBond; - type ProposalBondMinimum = ProposalBondMinimum; - type SpendPeriod = SpendPeriod; - type Burn = Burn; - type BurnDestination = (); - type ModuleId = TreasuryModuleId; - type SpendFunds = (); - type WeightInfo = (); + #[derive(Copy, Clone, Eq, PartialEq, Debug)] + struct BidPlaced { + height: u64, + bidder: u64, + para: ParaId, + first_slot: u64, + last_slot: u64, + amount: u64 } - thread_local! { - pub static PARACHAIN_COUNT: RefCell = RefCell::new(0); - pub static PARACHAINS: - RefCell> = RefCell::new(HashMap::new()); + static AUCTION: RefCell> = RefCell::new(None); + static ENDING_PERIOD: RefCell = RefCell::new(5); + static BIDS_PLACED: RefCell> = RefCell::new(Vec::new()); + } + #[allow(unused)] + fn set_ending_period(ending_period: u64) { + ENDING_PERIOD.with(|p| *p.borrow_mut() = ending_period); + } + fn auction() -> Option<(u64, u64)> { + AUCTION.with(|p| p.borrow().clone()) + } + fn ending_period() -> u64 { + ENDING_PERIOD.with(|p| p.borrow().clone()) + } + fn bids() -> Vec { + BIDS_PLACED.with(|p| p.borrow().clone()) } - const MAX_CODE_SIZE: u32 = 100; - const MAX_HEAD_DATA_SIZE: u32 = 10; + pub struct TestAuctioneer; + impl Auctioneer for TestAuctioneer { + type AccountId = u64; + type BlockNumber = u64; + type LeasePeriod = u64; + type Currency = Balances; - pub struct TestParachains; - impl Registrar for TestParachains { - fn new_id() -> ParaId { - PARACHAIN_COUNT.with(|p| { - *p.borrow_mut() += 1; - (*p.borrow() - 1).into() - }) - } + fn new_auction(duration: u64, lease_period_index: u64) -> DispatchResult { + assert!(lease_period_index >= Self::lease_period_index()); - fn head_data_size_allowed(head_data_size: u32) -> bool { - head_data_size <= MAX_HEAD_DATA_SIZE + let ending = System::block_number().saturating_add(duration); + AUCTION.with(|p| *p.borrow_mut() = Some((lease_period_index, ending))); + Ok(()) } - fn code_size_allowed(code_size: u32) -> bool { - code_size <= MAX_CODE_SIZE + fn is_ending(now: u64) -> Option { + if let Some((_, early_end)) = auction() { + if let Some(after_early_end) = now.checked_sub(early_end) { + if after_early_end < ending_period() { + return Some(after_early_end) + } + } + } + None } - fn register_para( - id: ParaId, - _parachain: bool, - code: ValidationCode, - initial_head_data: HeadData, + fn place_bid( + bidder: u64, + para: ParaId, + first_slot: u64, + last_slot: u64, + amount: u64 ) -> DispatchResult { - PARACHAINS.with(|p| { - if p.borrow().contains_key(&id.into()) { - panic!("ID already exists") - } - p.borrow_mut().insert(id.into(), (code, initial_head_data)); - Ok(()) - }) + let height = System::block_number(); + BIDS_PLACED.with(|p| p.borrow_mut().push(BidPlaced { height, bidder, para, first_slot, last_slot, amount })); + Ok(()) } - fn deregister_para(id: ParaId) -> DispatchResult { - PARACHAINS.with(|p| { - if !p.borrow().contains_key(&id.into()) { - panic!("ID doesn't exist") - } - p.borrow_mut().remove(&id.into()); - Ok(()) - }) + fn lease_period_index() -> u64 { + System::block_number() / 20 } } - parameter_types!{ - pub const LeasePeriod: u64 = 10; - pub const EndingPeriod: u64 = 3; - } - impl slots::Config for Test { - type Event = Event; - type Currency = Balances; - type Parachains = TestParachains; - type LeasePeriod = LeasePeriod; - type EndingPeriod = EndingPeriod; - type Randomness = RandomnessCollectiveFlip; - } parameter_types! { pub const SubmissionDeposit: u64 = 1; pub const MinContribution: u64 = 10; @@ -794,18 +753,21 @@ mod tests { pub const CrowdloanModuleId: ModuleId = ModuleId(*b"py/cfund"); pub const RemoveKeysLimit: u32 = 10; } + impl Config for Test { type Event = Event; type SubmissionDeposit = SubmissionDeposit; type MinContribution = MinContribution; type RetirementPeriod = RetirementPeriod; - type OrphanedFunds = Treasury; + type OrphanedFunds = (); type ModuleId = CrowdloanModuleId; type RemoveKeysLimit = RemoveKeysLimit; + type Registrar = TestRegistrar; + type Auctioneer = TestAuctioneer; + type WeightInfo = crate::crowdloan::TestWeightInfo; } use pallet_balances::Error as BalancesError; - use slots::Error as SlotsError; // This function basically just builds a genesis storage key/value store according to // our desired mockup. @@ -820,18 +782,24 @@ mod tests { t } + fn new_para() -> ParaId { + for i in 0.. { + let para: ParaId = i.into(); + if TestRegistrar::::is_registered(para) { continue } + assert_ok!(TestRegistrar::::register(1, para, Default::default(), Default::default())); + return para; + } + unreachable!() + } + fn run_to_block(n: u64) { while System::block_number() < n { Crowdloan::on_finalize(System::block_number()); - Treasury::on_finalize(System::block_number()); - Slots::on_finalize(System::block_number()); Balances::on_finalize(System::block_number()); System::on_finalize(System::block_number()); System::set_block_number(System::block_number() + 1); System::on_initialize(System::block_number()); Balances::on_initialize(System::block_number()); - Slots::on_initialize(System::block_number()); - Treasury::on_initialize(System::block_number()); Crowdloan::on_initialize(System::block_number()); } } @@ -840,25 +808,35 @@ mod tests { fn basic_setup_works() { new_test_ext().execute_with(|| { assert_eq!(System::block_number(), 0); - assert_eq!(Crowdloan::fund_count(), 0); - assert_eq!(Crowdloan::funds(0), None); - let empty: Vec = Vec::new(); + assert_eq!(Crowdloan::funds(ParaId::from(0)), None); + let empty: Vec = Vec::new(); assert_eq!(Crowdloan::new_raise(), empty); - assert_eq!(Crowdloan::contribution_get(0, &1), 0); + assert_eq!(Crowdloan::contribution_get(0u32, &1), 0); assert_eq!(Crowdloan::endings_count(), 0); + + assert_ok!(TestAuctioneer::new_auction(5, 0)); + + assert_eq!(bids(), vec![]); + assert_ok!(TestAuctioneer::place_bid(1, 2.into(), 0, 3, 6)); + let b = BidPlaced { height: 0, bidder: 1, para: 2.into(), first_slot: 0, last_slot: 3, amount: 6 }; + assert_eq!(bids(), vec![b]); + assert_eq!(TestAuctioneer::is_ending(4), None); + assert_eq!(TestAuctioneer::is_ending(5), Some(0)); + assert_eq!(TestAuctioneer::is_ending(9), Some(4)); + assert_eq!(TestAuctioneer::is_ending(11), None); }); } #[test] fn create_works() { new_test_ext().execute_with(|| { + let para = new_para(); // Now try to create a crowdloan campaign - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - assert_eq!(Crowdloan::fund_count(), 1); + assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, None)); // This is what the initial `fund_info` should look like let fund_info = FundInfo { - parachain: None, - owner: 1, + retiring: false, + depositor: 1, verifier: None, deposit: 1, raised: 0, @@ -868,15 +846,15 @@ mod tests { last_contribution: LastContribution::Never, first_slot: 1, last_slot: 4, - deploy_data: None, + trie_index: 0, }; - assert_eq!(Crowdloan::funds(0), Some(fund_info)); + assert_eq!(Crowdloan::funds(para), Some(fund_info)); // User has deposit removed from their free balance assert_eq!(Balances::free_balance(1), 999); - // Deposit is placed in crowdloan free balance - assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 1); + // Deposit is placed in reserved + assert_eq!(Balances::reserved_balance(1), 1); // No new raise until first contribution - let empty: Vec = Vec::new(); + let empty: Vec = Vec::new(); assert_eq!(Crowdloan::new_raise(), empty); }); } @@ -885,13 +863,13 @@ mod tests { fn create_with_verifier_works() { new_test_ext().execute_with(|| { let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); + let para = new_para(); // Now try to create a crowdloan campaign - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, Some(pubkey.clone()))); - assert_eq!(Crowdloan::fund_count(), 1); + assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, Some(pubkey.clone()))); // This is what the initial `fund_info` should look like let fund_info = FundInfo { - parachain: None, - owner: 1, + retiring: false, + depositor: 1, verifier: Some(pubkey), deposit: 1, raised: 0, @@ -901,15 +879,15 @@ mod tests { last_contribution: LastContribution::Never, first_slot: 1, last_slot: 4, - deploy_data: None, + trie_index: 0, }; - assert_eq!(Crowdloan::funds(0), Some(fund_info)); + assert_eq!(Crowdloan::funds(ParaId::from(0)), Some(fund_info)); // User has deposit removed from their free balance assert_eq!(Balances::free_balance(1), 999); - // Deposit is placed in crowdloan free balance - assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 1); + // Deposit is placed in reserved + assert_eq!(Balances::reserved_balance(1), 1); // No new raise until first contribution - let empty: Vec = Vec::new(); + let empty: Vec = Vec::new(); assert_eq!(Crowdloan::new_raise(), empty); }); } @@ -917,47 +895,47 @@ mod tests { #[test] fn create_handles_basic_errors() { new_test_ext().execute_with(|| { + // Now try to create a crowdloan campaign + let para = new_para(); + + let e = Error::::InvalidParaId; + assert_noop!(Crowdloan::create(Origin::signed(1), 1.into(), 1000, 1, 4, 9, None), e); // Cannot create a crowdloan with bad slots - assert_noop!( - Crowdloan::create(Origin::signed(1), 1000, 4, 1, 9, None), - Error::::LastSlotBeforeFirstSlot - ); - assert_noop!( - Crowdloan::create(Origin::signed(1), 1000, 1, 5, 9, None), - Error::::LastSlotTooFarInFuture - ); + let e = Error::::LastSlotBeforeFirstSlot; + assert_noop!(Crowdloan::create(Origin::signed(1), para, 1000, 4, 1, 9, None), e); + let e = Error::::LastSlotTooFarInFuture; + assert_noop!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 5, 9, None), e); // Cannot create a crowdloan without some deposit funds - assert_noop!( - Crowdloan::create(Origin::signed(1337), 1000, 1, 3, 9, None), - BalancesError::::InsufficientBalance - ); + assert_ok!(TestRegistrar::::register(1337, ParaId::from(1234), Default::default(), Default::default())); + let e = BalancesError::::InsufficientBalance; + assert_noop!(Crowdloan::create(Origin::signed(1337), ParaId::from(1234), 1000, 1, 3, 9, None), e); }); } #[test] fn contribute_works() { new_test_ext().execute_with(|| { + let para = new_para(); + // Set up a crowdloan - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - assert_eq!(Balances::free_balance(1), 999); - assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 1); + assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, None)); // No contributions yet - assert_eq!(Crowdloan::contribution_get(0, &1), 0); + assert_eq!(Crowdloan::contribution_get(u32::from(para), &1), 0); // User 1 contributes to their own crowdloan - assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 49, None)); + assert_ok!(Crowdloan::contribute(Origin::signed(1), para, 49, None)); // User 1 has spent some funds to do this, transfer fees **are** taken assert_eq!(Balances::free_balance(1), 950); // Contributions are stored in the trie - assert_eq!(Crowdloan::contribution_get(0, &1), 49); + assert_eq!(Crowdloan::contribution_get(u32::from(para), &1), 49); // Contributions appear in free balance of crowdloan - assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 50); + assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(para)), 49); // Crowdloan is added to NewRaise - assert_eq!(Crowdloan::new_raise(), vec![0]); + assert_eq!(Crowdloan::new_raise(), vec![para]); - let fund = Crowdloan::funds(0).unwrap(); + let fund = Crowdloan::funds(para).unwrap(); // Last contribution time recorded assert_eq!(fund.last_contribution, LastContribution::PreEnding(0)); @@ -968,43 +946,45 @@ mod tests { #[test] fn contribute_with_verifier_works() { new_test_ext().execute_with(|| { + let para = new_para(); let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); // Set up a crowdloan - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, Some(pubkey.clone()))); - assert_eq!(Balances::free_balance(1), 999); - assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 1); + assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, Some(pubkey.clone()))); + + // No contributions yet + assert_eq!(Crowdloan::contribution_get(u32::from(para), &1), 0); // Missing signature - assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 49, None), Error::::InvalidSignature); + assert_noop!(Crowdloan::contribute(Origin::signed(1), para, 49, None), Error::::InvalidSignature); let payload = (0u32, 1u64, 0u64, 49u64); let valid_signature = crypto::create_ed25519_signature(&payload.encode(), pubkey.clone()); let invalid_signature = MultiSignature::default(); // Invalid signature - assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 49, Some(invalid_signature)), Error::::InvalidSignature); + assert_noop!(Crowdloan::contribute(Origin::signed(1), para, 49, Some(invalid_signature)), Error::::InvalidSignature); // Valid signature wrong parameter - assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 50, Some(valid_signature.clone())), Error::::InvalidSignature); - assert_noop!(Crowdloan::contribute(Origin::signed(2), 0, 49, Some(valid_signature.clone())), Error::::InvalidSignature); + assert_noop!(Crowdloan::contribute(Origin::signed(1), para, 50, Some(valid_signature.clone())), Error::::InvalidSignature); + assert_noop!(Crowdloan::contribute(Origin::signed(2), para, 49, Some(valid_signature.clone())), Error::::InvalidSignature); // Valid signature - assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 49, Some(valid_signature.clone()))); + assert_ok!(Crowdloan::contribute(Origin::signed(1), para, 49, Some(valid_signature.clone()))); // Reuse valid signature - assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 49, Some(valid_signature)), Error::::InvalidSignature); + assert_noop!(Crowdloan::contribute(Origin::signed(1), para, 49, Some(valid_signature)), Error::::InvalidSignature); let payload_2 = (0u32, 1u64, 49u64, 10u64); let valid_signature_2 = crypto::create_ed25519_signature(&payload_2.encode(), pubkey); // New valid signature - assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 10, Some(valid_signature_2))); + assert_ok!(Crowdloan::contribute(Origin::signed(1), para, 10, Some(valid_signature_2))); // Contributions appear in free balance of crowdloan - assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 60); + assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(para)), 59); // Contribution amount is correct - let fund = Crowdloan::funds(0).unwrap(); + let fund = Crowdloan::funds(para).unwrap(); assert_eq!(fund.raised, 59); }); } @@ -1012,545 +992,252 @@ mod tests { #[test] fn contribute_handles_basic_errors() { new_test_ext().execute_with(|| { + let para = new_para(); + // Cannot contribute to non-existing fund - assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 49, None), Error::::InvalidFundIndex); + assert_noop!(Crowdloan::contribute(Origin::signed(1), para, 49, None), Error::::InvalidParaId); // Cannot contribute below minimum contribution - assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 9, None), Error::::ContributionTooSmall); + assert_noop!(Crowdloan::contribute(Origin::signed(1), para, 9, None), Error::::ContributionTooSmall); // Set up a crowdloan - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 101, None)); + assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, None)); + assert_ok!(Crowdloan::contribute(Origin::signed(1), para, 101, None)); // Cannot contribute past the limit - assert_noop!(Crowdloan::contribute(Origin::signed(2), 0, 900, None), Error::::CapExceeded); + assert_noop!(Crowdloan::contribute(Origin::signed(2), para, 900, None), Error::::CapExceeded); // Move past end date run_to_block(10); // Cannot contribute to ended fund - assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 49, None), Error::::ContributionPeriodOver); + assert_noop!(Crowdloan::contribute(Origin::signed(1), para, 49, None), Error::::ContributionPeriodOver); }); } #[test] - fn fix_deploy_data_works() { + fn bidding_works() { new_test_ext().execute_with(|| { - // Set up a crowdloan - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - assert_eq!(Balances::free_balance(1), 999); + let para = new_para(); - // Add deploy data - assert_ok!(Crowdloan::fix_deploy_data( - Origin::signed(1), - 0, - ::Hash::default(), - 0, - vec![0].into() - )); - - let fund = Crowdloan::funds(0).unwrap(); - - // Confirm deploy data is stored correctly - assert_eq!( - fund.deploy_data, - Some(DeployData { - code_hash: ::Hash::default(), - code_size: 0, - initial_head_data: vec![0].into(), - }), - ); - }); - } - - #[test] - fn fix_deploy_data_handles_basic_errors() { - new_test_ext().execute_with(|| { - // Set up a crowdloan - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - assert_eq!(Balances::free_balance(1), 999); + let first_slot = 1; + let last_slot = 4; - // Cannot set deploy data by non-owner - assert_noop!(Crowdloan::fix_deploy_data( - Origin::signed(2), - 0, - ::Hash::default(), - 0, - vec![0].into()), - Error::::InvalidOrigin - ); + assert_ok!(TestAuctioneer::new_auction(5, 0)); - // Cannot set deploy data to an invalid index - assert_noop!(Crowdloan::fix_deploy_data( - Origin::signed(1), - 1, - ::Hash::default(), - 0, - vec![0].into()), - Error::::InvalidFundIndex - ); - - // Cannot set deploy data after it already has been set - assert_ok!(Crowdloan::fix_deploy_data( - Origin::signed(1), - 0, - ::Hash::default(), - 0, - vec![0].into(), - )); - - assert_noop!(Crowdloan::fix_deploy_data( - Origin::signed(1), - 0, - ::Hash::default(), - 0, - vec![1].into()), - Error::::ExistingDeployData - ); - }); - } - - #[test] - fn onboard_works() { - new_test_ext().execute_with(|| { // Set up a crowdloan - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - assert_eq!(Balances::free_balance(1), 999); - - // Add deploy data - assert_ok!(Crowdloan::fix_deploy_data( - Origin::signed(1), - 0, - ::Hash::default(), - 0, - vec![0].into(), - )); + assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, first_slot, last_slot, 9, None)); + let bidder = Crowdloan::fund_account_id(para); // Fund crowdloan - assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 1000, None)); - + run_to_block(1); + assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None)); + run_to_block(3); + assert_ok!(Crowdloan::contribute(Origin::signed(3), para, 150, None)); + run_to_block(5); + assert_ok!(Crowdloan::contribute(Origin::signed(4), para, 200, None)); + run_to_block(8); + assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 250, None)); run_to_block(10); + assert_eq!(bids(), vec![ + BidPlaced { height: 5, amount: 250, bidder, para, first_slot, last_slot }, + BidPlaced { height: 6, amount: 450, bidder, para, first_slot, last_slot }, + BidPlaced { height: 9, amount: 700, bidder, para, first_slot, last_slot }, + ]); + // Endings count incremented assert_eq!(Crowdloan::endings_count(), 1); - - // Onboard crowdloan - assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); - - let fund = Crowdloan::funds(0).unwrap(); - // Crowdloan is now assigned a parachain id - assert_eq!(fund.parachain, Some(0.into())); - // This parachain is managed by Slots - assert_eq!(Slots::managed_ids(), vec![0.into()]); }); } #[test] - fn onboard_handles_basic_errors() { + fn withdraw_from_failed_works() { new_test_ext().execute_with(|| { - // Set up a crowdloan - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - assert_eq!(Balances::free_balance(1), 999); + let para = new_para(); - // Fund crowdloan - assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 1000, None)); + // Set up two crowdloans + assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 1, 9, None)); + assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None)); + assert_ok!(Crowdloan::contribute(Origin::signed(3), para, 50, None)); run_to_block(10); + let account_id = Crowdloan::fund_account_id(para); + // para has no reserved funds, indicating it did ot win the auction. + assert_eq!(Balances::reserved_balance(&account_id), 0); + // but there's still the funds in its balance. + assert_eq!(Balances::free_balance(&account_id), 150); + assert_eq!(Balances::free_balance(2), 1900); + assert_eq!(Balances::free_balance(3), 2950); + + assert_ok!(Crowdloan::withdraw(Origin::signed(2), 2, para)); + assert_eq!(Balances::free_balance(&account_id), 50); + assert_eq!(Balances::free_balance(2), 2000); - // Cannot onboard invalid fund index - assert_noop!(Crowdloan::onboard(Origin::signed(1), 1, 0.into()), Error::::InvalidFundIndex); - // Cannot onboard crowdloan without deploy data - assert_noop!(Crowdloan::onboard(Origin::signed(1), 0, 0.into()), Error::::UnsetDeployData); - - // Add deploy data - assert_ok!(Crowdloan::fix_deploy_data( - Origin::signed(1), - 0, - ::Hash::default(), - 0, - vec![0].into(), - )); - - // Cannot onboard fund with incorrect parachain id - assert_noop!(Crowdloan::onboard(Origin::signed(1), 0, 1.into()), SlotsError::::ParaNotOnboarding); - - // Onboard crowdloan - assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); - - // Cannot onboard fund again - assert_noop!(Crowdloan::onboard(Origin::signed(1), 0, 0.into()), Error::::AlreadyOnboard); + assert_ok!(Crowdloan::withdraw(Origin::signed(2), 3, para)); + assert_eq!(Balances::free_balance(&account_id), 0); + assert_eq!(Balances::free_balance(3), 3000); }); } #[test] - fn begin_retirement_works() { + fn dissolving_failed_without_contributions_works() { new_test_ext().execute_with(|| { - // Set up a crowdloan - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - assert_eq!(Balances::free_balance(1), 999); - - // Add deploy data - assert_ok!(Crowdloan::fix_deploy_data( - Origin::signed(1), - 0, - ::Hash::default(), - 0, - vec![0].into(), - )); - - // Fund crowdloan - assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 1000, None)); + let para = new_para(); + // Set up two crowdloans + assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 1, 9, None)); + assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None)); run_to_block(10); + assert_noop!(Crowdloan::dissolve(Origin::signed(2), para), Error::::NotReadyToDissolve); - // Onboard crowdloan - assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); - // Fund is assigned a parachain id - let fund = Crowdloan::funds(0).unwrap(); - assert_eq!(fund.parachain, Some(0.into())); - - // Off-boarding is set to the crowdloan account - assert_eq!(Slots::offboarding(ParaId::from(0)), Crowdloan::fund_account_id(0)); - - run_to_block(50); - - // Retire crowdloan to remove parachain id - assert_ok!(Crowdloan::begin_retirement(Origin::signed(1), 0)); - - // Fund should no longer have parachain id - let fund = Crowdloan::funds(0).unwrap(); - assert_eq!(fund.parachain, None); - + assert_ok!(Crowdloan::withdraw(Origin::signed(2), 2, para)); + assert_ok!(Crowdloan::dissolve(Origin::signed(1), para)); + assert_eq!(Balances::free_balance(1), 1000); }); } #[test] - fn begin_retirement_handles_basic_errors() { + fn dissolving_failed_with_contributions_works() { new_test_ext().execute_with(|| { - // Set up a crowdloan - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - assert_eq!(Balances::free_balance(1), 999); + let para = new_para(); + let issuance = Balances::total_issuance(); - // Add deploy data - assert_ok!(Crowdloan::fix_deploy_data( - Origin::signed(1), - 0, - ::Hash::default(), - 0, - vec![0].into(), - )); - - // Fund crowdloan - assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 1000, None)); + // Set up two crowdloans + assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 1, 9, None)); + assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None)); + assert_ok!(Crowdloan::contribute(Origin::signed(3), para, 50, None)); run_to_block(10); + assert_ok!(Crowdloan::withdraw(Origin::signed(2), 2, para)); - // Cannot retire fund that is not onboarded - assert_noop!(Crowdloan::begin_retirement(Origin::signed(1), 0), Error::::NotParachain); - - // Onboard crowdloan - assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); - // Fund is assigned a parachain id - let fund = Crowdloan::funds(0).unwrap(); - assert_eq!(fund.parachain, Some(0.into())); + run_to_block(14); + assert_noop!(Crowdloan::dissolve(Origin::signed(1), para), Error::::NotReadyToDissolve); - // Cannot retire fund whose deposit has not been returned - assert_noop!(Crowdloan::begin_retirement(Origin::signed(1), 0), Error::::ParaHasDeposit); - - run_to_block(50); - - // Cannot retire invalid fund index - assert_noop!(Crowdloan::begin_retirement(Origin::signed(1), 1), Error::::InvalidFundIndex); - - // Cannot retire twice - assert_ok!(Crowdloan::begin_retirement(Origin::signed(1), 0)); - assert_noop!(Crowdloan::begin_retirement(Origin::signed(1), 0), Error::::NotParachain); + run_to_block(15); + assert_ok!(Crowdloan::dissolve(Origin::signed(1), para)); + assert_eq!(Balances::free_balance(1), 1000); + assert_eq!(Balances::total_issuance(), issuance - 50); }); } #[test] - fn withdraw_works() { + fn withdraw_from_finished_works() { new_test_ext().execute_with(|| { - // Set up a crowdloan - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - // Transfer fee is taken here - assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 100, None)); - assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 200, None)); - assert_ok!(Crowdloan::contribute(Origin::signed(3), 0, 300, None)); - - // Skip all the way to the end - run_to_block(50); - - // Anyone can trigger withdraw of a user's balance without fees - assert_ok!(Crowdloan::withdraw(Origin::signed(1337), 1, 0)); - assert_eq!(Balances::free_balance(1), 999); + let para = new_para(); + let account_id = Crowdloan::fund_account_id(para); + + // Set up two crowdloans + assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 1, 9, None)); + + // Fund crowdloans. + assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None)); + assert_ok!(Crowdloan::contribute(Origin::signed(3), para, 50, None)); + // simulate the reserving of para's funds. this actually + // happens in the Slots pallet. + assert_ok!(Balances::reserve(&account_id, 150)); + + run_to_block(19); + assert_noop!(Crowdloan::withdraw(Origin::signed(2), 2, para), Error::::BidOrLeaseActive); - assert_ok!(Crowdloan::withdraw(Origin::signed(1337), 2, 0)); + run_to_block(20); + // simulate the unreserving of para's funds, now that the lease expired. this actually + // happens in the Slots pallet. + Balances::unreserve(&account_id, 150); + + // para has no reserved funds, indicating it did ot win the auction. + assert_eq!(Balances::reserved_balance(&account_id), 0); + // but there's still the funds in its balance. + assert_eq!(Balances::free_balance(&account_id), 150); + assert_eq!(Balances::free_balance(2), 1900); + assert_eq!(Balances::free_balance(3), 2950); + + assert_ok!(Crowdloan::withdraw(Origin::signed(2), 2, para)); + assert_eq!(Balances::free_balance(&account_id), 50); assert_eq!(Balances::free_balance(2), 2000); - assert_ok!(Crowdloan::withdraw(Origin::signed(1337), 3, 0)); + assert_ok!(Crowdloan::withdraw(Origin::signed(2), 3, para)); + assert_eq!(Balances::free_balance(&account_id), 0); assert_eq!(Balances::free_balance(3), 3000); }); } #[test] - fn withdraw_handles_basic_errors() { + fn dissolving_finished_without_contributions_works() { new_test_ext().execute_with(|| { - // Set up a crowdloan - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - // Transfer fee is taken here - assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 49, None)); - assert_eq!(Balances::free_balance(1), 950); - - run_to_block(5); - - // Cannot withdraw before fund ends - assert_noop!(Crowdloan::withdraw(Origin::signed(1337), 1, 0), Error::::FundNotEnded); + let para = new_para(); - run_to_block(10); + // Set up two crowdloans + assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 1, 9, None)); + // Fund crowdloans. + assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None)); + // during this time the funds get reserved and unreserved. + run_to_block(20); + assert_noop!(Crowdloan::dissolve(Origin::signed(2), para), Error::::NotReadyToDissolve); - // Cannot withdraw if they did not contribute - assert_noop!(Crowdloan::withdraw(Origin::signed(1337), 2, 0), Error::::NoContributions); - // Cannot withdraw from a non-existent fund - assert_noop!(Crowdloan::withdraw(Origin::signed(1337), 2, 1), Error::::InvalidFundIndex); + assert_ok!(Crowdloan::withdraw(Origin::signed(2), 2, para)); + assert_ok!(Crowdloan::dissolve(Origin::signed(1), para)); + assert_eq!(Balances::free_balance(1), 1000); }); } #[test] - fn dissolve_works() { + fn dissolving_finished_with_contributions_works() { new_test_ext().execute_with(|| { - // Set up a crowdloan - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - // Transfer fee is taken here - assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 100, None)); - assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 200, None)); - assert_ok!(Crowdloan::contribute(Origin::signed(3), 0, 300, None)); - - // Skip all the way to the end - run_to_block(50); - - // Check initiator's balance. - assert_eq!(Balances::free_balance(1), 899); - // Check current funds (contributions + deposit) - assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 601); - - // Dissolve the crowdloan - assert_ok!(Crowdloan::dissolve(Origin::signed(1), 0)); - - // Fund account is emptied - assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 0); - // Deposit is returned - assert_eq!(Balances::free_balance(1), 900); - // Treasury account is filled - assert_eq!(Balances::free_balance(Treasury::account_id()), 600); - - // Storage trie is removed - assert_eq!(Crowdloan::contribution_get(0,&0), 0); - // Fund storage is removed - assert_eq!(Crowdloan::funds(0), None); - - }); - } + let para = new_para(); + let issuance = Balances::total_issuance(); - #[test] - fn partial_dissolve_works() { - let mut ext = new_test_ext(); - ext.execute_with(|| { // Set up a crowdloan - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdloan::create(Origin::signed(1), 100_000, 1, 4, 9, None)); - - // Add lots of contributors, beyond what we can delete in one go. - for i in 0 .. 30 { - Balances::make_free_balance_be(&i, 300); - assert_ok!(Crowdloan::contribute(Origin::signed(i), 0, 100, None)); - assert_eq!(Crowdloan::contribution_get(0, &i), 100); - } - - // Skip all the way to the end - run_to_block(50); - - // Check current funds (contributions + deposit) - assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 100 * 30 + 1); - }); - - ext.commit_all().unwrap(); - ext.execute_with(|| { - // Partially dissolve the crowdloan - assert_ok!(Crowdloan::dissolve(Origin::signed(1), 0)); - for i in 0 .. 10 { - assert_eq!(Crowdloan::contribution_get(0, &i), 0); - } - for i in 10 .. 30 { - assert_eq!(Crowdloan::contribution_get(0, &i), 100); - } - }); - - ext.commit_all().unwrap(); - ext.execute_with(|| { - // Partially dissolve the crowdloan, again - assert_ok!(Crowdloan::dissolve(Origin::signed(1), 0)); - for i in 0 .. 20 { - assert_eq!(Crowdloan::contribution_get(0, &i), 0); - } - for i in 20 .. 30 { - assert_eq!(Crowdloan::contribution_get(0, &i), 100); - } - }); + assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 1, 9, None)); + assert_ok!(Crowdloan::contribute(Origin::signed(2), para, 100, None)); + assert_ok!(Crowdloan::contribute(Origin::signed(3), para, 50, None)); + run_to_block(20); + assert_ok!(Crowdloan::withdraw(Origin::signed(2), 2, para)); - ext.commit_all().unwrap(); - ext.execute_with(|| { - // Fully dissolve the crowdloan - assert_ok!(Crowdloan::dissolve(Origin::signed(1), 0)); - for i in 0 .. 30 { - assert_eq!(Crowdloan::contribution_get(0, &i), 0); - } + run_to_block(24); + assert_noop!(Crowdloan::dissolve(Origin::signed(1), para), Error::::NotReadyToDissolve); - // Fund account is emptied - assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 0); - // Deposit is returned - assert_eq!(Balances::free_balance(1), 201); - // Treasury account is filled - assert_eq!(Balances::free_balance(Treasury::account_id()), 100 * 30); - // Fund storage is removed - assert_eq!(Crowdloan::funds(0), None); + run_to_block(25); + assert_ok!(Crowdloan::dissolve(Origin::signed(1), para)); + assert_eq!(Balances::free_balance(1), 1000); + assert_eq!(Balances::total_issuance(), issuance - 50); }); } #[test] - fn dissolve_handles_basic_errors() { + fn on_swap_works() { new_test_ext().execute_with(|| { - // Set up a crowdloan - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - // Transfer fee is taken here - assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 100, None)); - assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 200, None)); - assert_ok!(Crowdloan::contribute(Origin::signed(3), 0, 300, None)); - - // Cannot dissolve an invalid fund index - assert_noop!(Crowdloan::dissolve(Origin::signed(1), 1), Error::::InvalidFundIndex); - // Cannot dissolve a fund in progress - assert_noop!(Crowdloan::dissolve(Origin::signed(1), 0), Error::::InRetirementPeriod); - - run_to_block(10); - - // Onboard fund - assert_ok!(Crowdloan::fix_deploy_data( - Origin::signed(1), - 0, - ::Hash::default(), - 0, - vec![0].into(), - )); - assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); - - // Cannot dissolve an active fund - assert_noop!(Crowdloan::dissolve(Origin::signed(1), 0), Error::::HasActiveParachain); + let para_1 = new_para(); + let para_2 = new_para(); + + // Set up crowdloans + assert_ok!(Crowdloan::create(Origin::signed(1), para_1, 1000, 1, 1, 9, None)); + assert_ok!(Crowdloan::create(Origin::signed(1), para_2, 1000, 1, 1, 9, None)); + // Different contributions + assert_ok!(Crowdloan::contribute(Origin::signed(2), para_1, 100, None)); + assert_ok!(Crowdloan::contribute(Origin::signed(3), para_2, 50, None)); + // Original state + assert_eq!(Funds::::get(para_1).unwrap().raised, 100); + assert_eq!(Funds::::get(para_2).unwrap().raised, 50); + // Swap + Crowdloan::on_swap(para_1, para_2); + // Final state + assert_eq!(Funds::::get(para_2).unwrap().raised, 100); + assert_eq!(Funds::::get(para_1).unwrap().raised, 50); }); } #[test] - fn fund_before_auction_works() { + fn cannot_create_fund_when_already_active() { new_test_ext().execute_with(|| { - // Create a crowdloan before an auction is created - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9, None)); - // Users can already contribute - assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 49, None)); - // Fund added to NewRaise - assert_eq!(Crowdloan::new_raise(), vec![0]); - - // Some blocks later... - run_to_block(2); - // Create an auction - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - // Add deploy data - assert_ok!(Crowdloan::fix_deploy_data( - Origin::signed(1), - 0, - ::Hash::default(), - 0, - vec![0].into(), - )); - // Move to the end of auction... - run_to_block(12); + let para_1 = new_para(); - // Endings count incremented - assert_eq!(Crowdloan::endings_count(), 1); - - // Onboard crowdloan - assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); + assert_ok!(Crowdloan::create(Origin::signed(1), para_1, 1000, 1, 1, 9, None)); + // Cannot create a fund again + assert_noop!( + Crowdloan::create(Origin::signed(1), para_1, 1000, 1, 1, 9, None), + Error::::FundNotEnded, + ); - let fund = Crowdloan::funds(0).unwrap(); - // Crowdloan is now assigned a parachain id - assert_eq!(fund.parachain, Some(0.into())); - // This parachain is managed by Slots - assert_eq!(Slots::managed_ids(), vec![0.into()]); - }); - } - - #[test] - fn fund_across_multiple_auctions_works() { - new_test_ext().execute_with(|| { - // Create an auction - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - // Create two competing crowdloans, with end dates across multiple auctions - // Each crowdloan is competing for the same slots, so only one can win - assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 30, None)); - assert_ok!(Crowdloan::create(Origin::signed(2), 1000, 1, 4, 30, None)); - - // Contribute to all, but more money to 0, less to 1 - assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 300, None)); - assert_ok!(Crowdloan::contribute(Origin::signed(1), 1, 200, None)); - - // Add deploy data to all - assert_ok!(Crowdloan::fix_deploy_data( - Origin::signed(1), - 0, - ::Hash::default(), - 0, - vec![0].into(), - )); - assert_ok!(Crowdloan::fix_deploy_data( - Origin::signed(2), - 1, - ::Hash::default(), - 0, - vec![0].into(), - )); - - // End the current auction, fund 0 wins! - run_to_block(10); - assert_eq!(Crowdloan::endings_count(), 1); - // Onboard crowdloan - assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); - let fund = Crowdloan::funds(0).unwrap(); - // Crowdloan is now assigned a parachain id - assert_eq!(fund.parachain, Some(0.into())); - // This parachain is managed by Slots - assert_eq!(Slots::managed_ids(), vec![0.into()]); - - // Create a second auction - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - // Contribute to existing funds add to NewRaise - assert_ok!(Crowdloan::contribute(Origin::signed(1), 1, 10, None)); - - // End the current auction, fund 1 wins! - run_to_block(20); - assert_eq!(Crowdloan::endings_count(), 2); - // Onboard crowdloan - assert_ok!(Crowdloan::onboard(Origin::signed(2), 1, 1.into())); - let fund = Crowdloan::funds(1).unwrap(); - // Crowdloan is now assigned a parachain id - assert_eq!(fund.parachain, Some(1.into())); - // This parachain is managed by Slots - assert_eq!(Slots::managed_ids(), vec![0.into(), 1.into()]); }); } } @@ -1558,20 +1245,15 @@ mod tests { #[cfg(feature = "runtime-benchmarks")] mod benchmarking { use super::{*, Module as Crowdloan}; - use crate::slots::Module as Slots; use frame_system::RawOrigin; use frame_support::{ assert_ok, traits::OnInitialize, }; - use sp_runtime::traits::Bounded; + use sp_runtime::traits::{Bounded, CheckedSub}; use sp_std::prelude::*; - use frame_benchmarking::{benchmarks, whitelisted_caller, account, whitelist_account}; - - // TODO: replace with T::Parachains::MAX_CODE_SIZE - const MAX_CODE_SIZE: u32 = 10; - const MAX_HEAD_DATA_SIZE: u32 = 10; + use frame_benchmarking::{benchmarks, whitelisted_caller, account, impl_benchmark_test_suite}; fn assert_last_event(generic_event: ::Event) { let events = frame_system::Module::::events(); @@ -1581,24 +1263,38 @@ mod benchmarking { assert_eq!(event, &system_event); } - fn create_fund(end: T::BlockNumber) -> FundIndex { + fn create_fund(id: u32, end: T::BlockNumber) -> ParaId { let cap = BalanceOf::::max_value(); - let lease_period_index = end / T::LeasePeriod::get(); + let lease_period_index = T::Auctioneer::lease_period_index(); let first_slot = lease_period_index; let last_slot = lease_period_index + 3u32.into(); + let para_id = id.into(); - let caller = account("fund_creator", 0, 0); - T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + let caller = account("fund_creator", id, 0); + CurrencyOf::::make_free_balance_be(&caller, BalanceOf::::max_value()); // Assume ed25519 is most complex signature format let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); - assert_ok!(Crowdloan::::create(RawOrigin::Signed(caller).into(), cap, first_slot, last_slot, end, Some(pubkey))); - FundCount::get() - 1 + let head_data = T::Registrar::worst_head_data(); + let validation_code = T::Registrar::worst_validation_code(); + assert_ok!(T::Registrar::register(caller.clone(), para_id, head_data, validation_code)); + + assert_ok!(Crowdloan::::create( + RawOrigin::Signed(caller).into(), + para_id, + cap, + first_slot, + last_slot, + end, + Some(pubkey) + )); + + para_id } - fn contribute_fund(who: &T::AccountId, index: FundIndex) { - T::Currency::make_free_balance_be(&who, BalanceOf::::max_value()); + fn contribute_fund(who: &T::AccountId, index: ParaId) { + CurrencyOf::::make_free_balance_be(&who, BalanceOf::::max_value()); let value = T::MinContribution::get(); let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); @@ -1608,86 +1304,35 @@ mod benchmarking { assert_ok!(Crowdloan::::contribute(RawOrigin::Signed(who.clone()).into(), index, value, Some(sig))); } - fn worst_validation_code() -> Vec { - // TODO: replace with T::Parachains::MAX_CODE_SIZE - let mut validation_code = vec![0u8; MAX_CODE_SIZE as usize]; - // Replace first bytes of code with "WASM_MAGIC" to pass validation test. - let _ = validation_code.splice( - ..crate::WASM_MAGIC.len(), - crate::WASM_MAGIC.iter().cloned(), - ).collect::>(); - validation_code - } - - fn worst_deploy_data() -> DeployData { - let validation_code = worst_validation_code::(); - let code = primitives::v1::ValidationCode(validation_code); - // TODO: replace with T::Parachains::MAX_HEAD_DATA_SIZE - let head_data = HeadData(vec![0u8; MAX_HEAD_DATA_SIZE as usize]); - - DeployData { - code_hash: T::Hashing::hash(&code.0), - // TODO: replace with T::Parachains::MAX_CODE_SIZE - code_size: MAX_CODE_SIZE, - initial_head_data: head_data, - } - } - - fn setup_onboarding( - fund_index: FundIndex, - para_id: ParaId, - end_block: T::BlockNumber, - ) -> DispatchResult { - // Matches fund creator in `create_fund` - let fund_creator = account("fund_creator", 0, 0); - let DeployData { code_hash, code_size, initial_head_data } = worst_deploy_data::(); - Crowdloan::::fix_deploy_data( - RawOrigin::Signed(fund_creator).into(), - fund_index, - code_hash, - code_size, - initial_head_data - )?; - - let lease_period_index = end_block / T::LeasePeriod::get(); - Slots::::new_auction(RawOrigin::Root.into(), end_block, lease_period_index)?; - let contributor: T::AccountId = account("contributor", 0, 0); - contribute_fund::(&contributor, fund_index); - - // TODO: Probably should use on_initialize - //Slots::::on_initialize(end_block + T::EndingPeriod::get()); - let onboarding_data = (lease_period_index, crate::slots::IncomingParachain::Unset( - crate::slots::NewBidder { - who: Crowdloan::::fund_account_id(fund_index), - sub: Default::default(), - } - )); - crate::slots::Onboarding::::insert(para_id, onboarding_data); - Ok(()) - } - benchmarks! { create { + let para_id = ParaId::from(1); let cap = BalanceOf::::max_value(); let first_slot = 0u32.into(); let last_slot = 3u32.into(); let end = T::BlockNumber::max_value(); let caller: T::AccountId = whitelisted_caller(); + let head_data = T::Registrar::worst_head_data(); + let validation_code = T::Registrar::worst_validation_code(); + let verifier = account("verifier", 0, 0); - T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); - }: _(RawOrigin::Signed(caller), cap, first_slot, last_slot, end, Some(verifier)) + CurrencyOf::::make_free_balance_be(&caller, BalanceOf::::max_value()); + T::Registrar::register(caller.clone(), para_id, head_data, validation_code)?; + + }: _(RawOrigin::Signed(caller), para_id, cap, first_slot, last_slot, end, Some(verifier)) verify { - assert_last_event::(RawEvent::Created(FundCount::get() - 1).into()) + assert_last_event::(RawEvent::Created(para_id).into()) } // Contribute has two arms: PreEnding and Ending, but both are equal complexity. contribute { - let fund_index = create_fund::(100u32.into()); + let fund_index = create_fund::(1, 100u32.into()); let caller: T::AccountId = whitelisted_caller(); let contribution = T::MinContribution::get(); - T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + CurrencyOf::::make_free_balance_be(&caller, BalanceOf::::max_value()); + assert!(NewRaise::get().is_empty()); let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); let payload = (fund_index, &caller, BalanceOf::::default(), contribution); @@ -1700,51 +1345,8 @@ mod benchmarking { assert_last_event::(RawEvent::Contributed(caller, fund_index, contribution).into()); } - fix_deploy_data { - let fund_index = create_fund::(100u32.into()); - // Matches fund creator in `create_fund` - let caller = account("fund_creator", 0, 0); - - let DeployData { code_hash, code_size, initial_head_data } = worst_deploy_data::(); - - whitelist_account!(caller); - }: _(RawOrigin::Signed(caller), fund_index, code_hash, code_size, initial_head_data) - verify { - assert_last_event::(RawEvent::DeployDataFixed(fund_index).into()); - } - - onboard { - let end_block: T::BlockNumber = 100u32.into(); - let fund_index = create_fund::(end_block); - let para_id = Default::default(); - - setup_onboarding::(fund_index, para_id, end_block)?; - - let caller = whitelisted_caller(); - }: _(RawOrigin::Signed(caller), fund_index, para_id) - verify { - assert_last_event::(RawEvent::Onboarded(fund_index, para_id).into()); - } - - begin_retirement { - let end_block: T::BlockNumber = 100u32.into(); - let fund_index = create_fund::(end_block); - let para_id = Default::default(); - - setup_onboarding::(fund_index, para_id, end_block)?; - - let caller: T::AccountId = whitelisted_caller(); - Crowdloan::::onboard(RawOrigin::Signed(caller.clone()).into(), fund_index, para_id)?; - - // Remove deposits to look like it is off-boarded - crate::slots::Deposits::::remove(para_id); - }: _(RawOrigin::Signed(caller), fund_index) - verify { - assert_last_event::(RawEvent::Retiring(fund_index).into()); - } - withdraw { - let fund_index = create_fund::(100u32.into()); + let fund_index = create_fund::(1, 100u32.into()); let caller: T::AccountId = whitelisted_caller(); let contributor = account("contributor", 0, 0); contribute_fund::(&contributor, fund_index); @@ -1756,15 +1358,24 @@ mod benchmarking { // Worst case: Dissolve removes `RemoveKeysLimit` keys, and then finishes up the dissolution of the fund. dissolve { - let fund_index = create_fund::(100u32.into()); + let k in 0 .. T::RemoveKeysLimit::get(); + let fund_index = create_fund::(1, 100u32.into()); // Dissolve will remove at most `RemoveKeysLimit` at once. - for i in 0 .. T::RemoveKeysLimit::get() { + for i in 0 .. k { contribute_fund::(&account("contributor", i, 0), fund_index); } + // One extra contributor so we can trigger withdraw + contribute_fund::(&account("last_contributor", 0, 0), fund_index); + let caller: T::AccountId = whitelisted_caller(); - frame_system::Module::::set_block_number(T::RetirementPeriod::get().saturating_add(200u32.into())); + frame_system::Module::::set_block_number(T::BlockNumber::max_value()); + Crowdloan::::withdraw( + RawOrigin::Signed(caller.clone()).into(), + account("last_contributor", 0, 0), + fund_index, + )?; }: _(RawOrigin::Signed(caller.clone()), fund_index) verify { assert_last_event::(RawEvent::Dissolved(fund_index).into()); @@ -1781,21 +1392,23 @@ mod benchmarking { let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); for i in 0 .. n { - let fund_index = create_fund::(end_block); + let fund_index = create_fund::(i, end_block); let contributor: T::AccountId = account("contributor", i, 0); let contribution = T::MinContribution::get() * (i + 1).into(); - T::Currency::make_free_balance_be(&contributor, BalanceOf::::max_value()); - let payload = (fund_index, &contributor, BalanceOf::::default(), contribution); let sig = crypto::create_ed25519_signature(&payload.encode(), pubkey.clone()); + CurrencyOf::::make_free_balance_be(&contributor, BalanceOf::::max_value()); Crowdloan::::contribute(RawOrigin::Signed(contributor).into(), fund_index, contribution, Some(sig))?; } - let lease_period_index = end_block / T::LeasePeriod::get(); - Slots::::new_auction(RawOrigin::Root.into(), end_block, lease_period_index)?; + let lease_period_index = T::Auctioneer::lease_period_index(); + let duration = end_block + .checked_sub(&frame_system::Module::::block_number()) + .ok_or("duration of auction less than zero")?; + T::Auctioneer::new_auction(duration, lease_period_index)?; - assert_eq!(>::is_ending(end_block), Some(0u32.into())); + assert_eq!(T::Auctioneer::is_ending(end_block), Some(0u32.into())); assert_eq!(NewRaise::get().len(), n as usize); let old_endings_count = EndingsCount::get(); }: { @@ -1806,23 +1419,9 @@ mod benchmarking { } } - #[cfg(test)] - mod tests { - use super::*; - use crate::crowdloan::tests::{new_test_ext, Test}; - - #[test] - fn test_benchmarks() { - new_test_ext().execute_with(|| { - assert_ok!(test_benchmark_create::()); - assert_ok!(test_benchmark_contribute::()); - assert_ok!(test_benchmark_fix_deploy_data::()); - assert_ok!(test_benchmark_onboard::()); - assert_ok!(test_benchmark_begin_retirement::()); - assert_ok!(test_benchmark_withdraw::()); - assert_ok!(test_benchmark_dissolve::()); - assert_ok!(test_benchmark_on_initialize::()); - }); - } - } + impl_benchmark_test_suite!( + Crowdloan, + crate::integration_tests::new_test_ext(), + crate::integration_tests::Test, + ); } diff --git a/runtime/common/src/integration_tests.rs b/runtime/common/src/integration_tests.rs new file mode 100644 index 000000000000..d75b8d482793 --- /dev/null +++ b/runtime/common/src/integration_tests.rs @@ -0,0 +1,882 @@ +// Copyright 2019-2021 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! Mocking utilities for testing with real pallets. + +use sp_std::sync::Arc; +use sp_io::TestExternalities; +use sp_core::{H256, crypto::KeyTypeId}; +use sp_runtime::{ + ModuleId, + traits::{ + BlakeTwo256, IdentityLookup, One, + }, +}; +use sp_keystore::{KeystoreExt, testing::KeyStore}; +use primitives::v1::{BlockNumber, Header, Id as ParaId, ValidationCode, HeadData}; +use frame_support::{ + parameter_types, assert_ok, assert_noop, + storage::StorageMap, + traits::{Currency, OnInitialize, OnFinalize, KeyOwnerProofSystem}, +}; +use frame_system::EnsureRoot; +use runtime_parachains::{ + ParaLifecycle, Origin as ParaOrigin, + paras, configuration, shared, +}; +use frame_support_test::TestRandomness; +use crate::{ + auctions, crowdloan, slots, paras_registrar, + traits::{ + Registrar as RegistrarT, Auctioneer, + }, +}; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +type AccountId = u32; +type Balance = u32; +type Moment = u32; + +frame_support::construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + // System Stuff + System: frame_system::{Module, Call, Config, Storage, Event}, + Balances: pallet_balances::{Module, Call, Storage, Config, Event}, + + // Parachains Runtime + Configuration: configuration::{Module, Call, Storage, Config}, + Paras: paras::{Module, Origin, Call, Storage, Config}, + + // Para Onboarding Pallets + Registrar: paras_registrar::{Module, Call, Storage, Event}, + Auctions: auctions::{Module, Call, Storage, Event}, + Crowdloan: crowdloan::{Module, Call, Storage, Event}, + Slots: slots::{Module, Call, Storage, Event}, + } +); + +use crate::crowdloan::Error as CrowdloanError; + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(4 * 1024 * 1024); +} + +impl frame_system::Config for Test { + type BaseCallFilter = (); + type BlockWeights = BlockWeights; + type BlockLength = (); + type DbWeight = (); + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); +} + +parameter_types! { + pub const EpochDuration: u64 = 10; + pub const ExpectedBlockTime: Moment = 6_000; + pub const ReportLongevity: u64 = 10; +} + +impl pallet_babe::Config for Test { + type EpochDuration = EpochDuration; + type ExpectedBlockTime = ExpectedBlockTime; + type EpochChangeTrigger = pallet_babe::ExternalTrigger; + type KeyOwnerProofSystem = (); + type KeyOwnerProof = + >::Proof; + type KeyOwnerIdentification = >::IdentificationTuple; + type HandleEquivocation = (); + type WeightInfo = (); +} + +parameter_types! { + pub const MinimumPeriod: Moment = 6_000 / 2; +} + +impl pallet_timestamp::Config for Test { + type Moment = Moment; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +parameter_types! { + pub static ExistentialDeposit: Balance = 1; +} + +impl pallet_balances::Config for Test { + type MaxLocks = (); + type Balance = Balance; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); +} + +impl configuration::Config for Test { } + +impl shared::Config for Test { } + +impl paras::Config for Test { + type Origin = Origin; +} + +parameter_types! { + pub const ParaDeposit: Balance = 500; + pub const DataDepositPerByte: Balance = 1; + pub const MaxCodeSize: u32 = 200; + pub const MaxHeadSize: u32 = 100; +} + +impl paras_registrar::Config for Test { + type Event = Event; + type OnSwap = (Crowdloan, Slots); + type ParaDeposit = ParaDeposit; + type DataDepositPerByte = DataDepositPerByte; + type MaxCodeSize = MaxCodeSize; + type MaxHeadSize = MaxHeadSize; + type Currency = Balances; + type Origin = Origin; + type WeightInfo = crate::paras_registrar::TestWeightInfo; +} + +parameter_types! { + pub const EndingPeriod: BlockNumber = 10; +} + +impl auctions::Config for Test { + type Event = Event; + type Leaser = Slots; + type EndingPeriod = EndingPeriod; + type Randomness = TestRandomness; + type InitiateOrigin = EnsureRoot; + type WeightInfo = crate::auctions::TestWeightInfo; +} + +parameter_types! { + pub const LeasePeriod: BlockNumber = 100; +} + +impl slots::Config for Test { + type Event = Event; + type Currency = Balances; + type Registrar = Registrar; + type LeasePeriod = LeasePeriod; + type WeightInfo = crate::slots::TestWeightInfo; +} + +parameter_types! { + pub const CrowdloanId: ModuleId = ModuleId(*b"py/cfund"); + pub const SubmissionDeposit: Balance = 100; + pub const MinContribution: Balance = 1; + pub const RetirementPeriod: BlockNumber = 10; + pub const RemoveKeysLimit: u32 = 100; + +} + +impl crowdloan::Config for Test { + type Event = Event; + type ModuleId = CrowdloanId; + type SubmissionDeposit = SubmissionDeposit; + type MinContribution = MinContribution; + type RetirementPeriod = RetirementPeriod; + type OrphanedFunds = (); + type RemoveKeysLimit = RemoveKeysLimit; + type Registrar = Registrar; + type Auctioneer = Auctions; + type WeightInfo = crate::crowdloan::TestWeightInfo; +} + +/// Create a new set of test externalities. +pub fn new_test_ext() -> TestExternalities { + let t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + let keystore = KeyStore::new(); + let mut ext: sp_io::TestExternalities = t.into(); + ext.register_extension(KeystoreExt(Arc::new(keystore))); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +const BLOCKS_PER_SESSION: u32 = 10; + +fn maybe_new_session(n: u32) { + if n % BLOCKS_PER_SESSION == 0 { + shared::Module::::set_session_index( + shared::Module::::session_index() + 1 + ); + Paras::test_on_new_session(); + } +} + +fn test_genesis_head(size: usize) -> HeadData { + HeadData(vec![0u8; size]) +} + +fn test_validation_code(size: usize) -> ValidationCode { + let mut validation_code = vec![0u8; size as usize]; + // Replace first bytes of code with "WASM_MAGIC" to pass validation test. + let _ = validation_code.splice( + ..crate::WASM_MAGIC.len(), + crate::WASM_MAGIC.iter().cloned(), + ).collect::>(); + ValidationCode(validation_code) +} + +fn para_origin(id: u32) -> ParaOrigin { + ParaOrigin::Parachain(id.into()) +} + +fn run_to_block(n: u32) { + assert!(System::block_number() < n); + while System::block_number() < n { + let block_number = System::block_number(); + AllModules::on_finalize(block_number); + System::on_finalize(block_number); + System::set_block_number(block_number + 1); + System::on_initialize(block_number + 1); + maybe_new_session(block_number + 1); + AllModules::on_initialize(block_number + 1); + } +} + +fn run_to_session(n: u32) { + let block_number = BLOCKS_PER_SESSION * n; + run_to_block(block_number); +} + +fn last_event() -> Event { + System::events().pop().expect("Event expected").event +} + +#[test] +fn basic_end_to_end_works() { + new_test_ext().execute_with(|| { + assert!(System::block_number().is_one()); + // User 1 and 2 will own parachains + Balances::make_free_balance_be(&1, 1_000); + Balances::make_free_balance_be(&2, 1_000); + // First register 2 parathreads + let genesis_head = Registrar::worst_head_data(); + let validation_code = Registrar::worst_validation_code(); + assert_ok!(Registrar::register( + Origin::signed(1), + ParaId::from(1), + genesis_head.clone(), + validation_code.clone(), + )); + assert_ok!(Registrar::register( + Origin::signed(2), + ParaId::from(2), + genesis_head, + validation_code, + )); + + // Paras should be onboarding + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Onboarding)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Onboarding)); + + // Start a new auction in the future + let duration = 99u32; + let lease_period_index_start = 4u32; + assert_ok!(Auctions::new_auction(Origin::root(), duration, lease_period_index_start)); + + // 2 sessions later they are parathreads + run_to_session(2); + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Parathread)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Parathread)); + + // Para 1 will bid directly for slot 1, 2 + // Open a crowdloan for Para 2 for slot 3, 4 + assert_ok!(Crowdloan::create( + Origin::signed(2), + ParaId::from(2), + 1_000, // Cap + lease_period_index_start + 2, // First Slot + lease_period_index_start + 3, // Last Slot + 200, // Block End + None, + )); + let crowdloan_account = Crowdloan::fund_account_id(ParaId::from(2)); + + // Auction ending begins on block 100, so we make a bid before then. + run_to_block(90); + + Balances::make_free_balance_be(&10, 1_000); + Balances::make_free_balance_be(&20, 1_000); + + // User 10 will bid directly for parachain 1 + assert_ok!(Auctions::bid( + Origin::signed(10), + ParaId::from(1), + 1, // Auction Index + lease_period_index_start + 0, // First Slot + lease_period_index_start + 1, // Last slot + 910, // Amount + )); + + // User 2 will be a contribute to crowdfund for parachain 2 + Balances::make_free_balance_be(&2, 1_000); + assert_ok!(Crowdloan::contribute(Origin::signed(2), ParaId::from(2), 920, None)); + + // Auction ends at block 110 + run_to_block(109); + assert_eq!( + last_event(), + crowdloan::RawEvent::HandleBidResult(ParaId::from(2), Ok(())).into(), + ); + run_to_block(110); + assert_eq!( + last_event(), + auctions::RawEvent::AuctionClosed(1).into(), + ); + + // Paras should have won slots + assert_eq!( + slots::Leases::::get(ParaId::from(1)), + // -- 1 --- 2 --- 3 --------- 4 ------------ 5 -------- + vec![None, None, None, Some((10, 910)), Some((10, 910))], + ); + assert_eq!( + slots::Leases::::get(ParaId::from(2)), + // -- 1 --- 2 --- 3 --- 4 --- 5 ---------------- 6 --------------------------- 7 ---------------- + vec![None, None, None, None, None, Some((crowdloan_account, 920)), Some((crowdloan_account, 920))], + ); + + // New leases will start on block 400 + let lease_start_block = 400; + run_to_block(lease_start_block); + + // First slot, Para 1 should be transitioning to Parachain + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::UpgradingParathread)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Parathread)); + + // Two sessions later, it has upgraded + run_to_block(lease_start_block + 20); + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Parachain)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Parathread)); + + // Second slot nothing happens :) + run_to_block(lease_start_block + 100); + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Parachain)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Parathread)); + + // Third slot, Para 2 should be upgrading, and Para 1 is downgrading + run_to_block(lease_start_block + 200); + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::DowngradingParachain)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::UpgradingParathread)); + + // Two sessions later, they have transitioned + run_to_block(lease_start_block + 220); + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Parathread)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Parachain)); + + // Fourth slot nothing happens :) + run_to_block(lease_start_block + 300); + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Parathread)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Parachain)); + + // Fifth slot, Para 2 is downgrading + run_to_block(lease_start_block + 400); + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Parathread)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::DowngradingParachain)); + + // Two sessions later, Para 2 is downgraded + run_to_block(lease_start_block + 420); + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Parathread)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Parathread)); + }); +} + +#[test] +fn basic_errors_fail() { + new_test_ext().execute_with(|| { + assert!(System::block_number().is_one()); + // Can't double register + Balances::make_free_balance_be(&1, 1_000); + Balances::make_free_balance_be(&2, 1_000); + + let genesis_head = Registrar::worst_head_data(); + let validation_code = Registrar::worst_validation_code(); + assert_ok!(Registrar::register( + Origin::signed(1), + ParaId::from(1), + genesis_head.clone(), + validation_code.clone(), + )); + assert_noop!(Registrar::register( + Origin::signed(2), + ParaId::from(1), + genesis_head, + validation_code, + ), paras_registrar::Error::::AlreadyRegistered); + + // Start an auction + let duration = 99u32; + let lease_period_index_start = 4u32; + assert_ok!(Auctions::new_auction(Origin::root(), duration, lease_period_index_start)); + + // Cannot create a crowdloan if you do not own the para + assert_noop!(Crowdloan::create( + Origin::signed(2), + ParaId::from(1), + 1_000, // Cap + lease_period_index_start + 2, // First Slot + lease_period_index_start + 3, // Last Slot + 200, // Block End + None, + ), crowdloan::Error::::InvalidOrigin); + }); +} + +#[test] +fn competing_slots() { + // This test will verify that competing slots, from different sources will resolve appropriately. + new_test_ext().execute_with(|| { + assert!(System::block_number().is_one()); + let max_bids = 10u32; + + // Create n paras and owners + for n in 1 ..= max_bids { + Balances::make_free_balance_be(&n, 1_000); + let genesis_head = Registrar::worst_head_data(); + let validation_code = Registrar::worst_validation_code(); + assert_ok!(Registrar::register( + Origin::signed(n), + ParaId::from(n), + genesis_head, + validation_code, + )); + } + + // Start a new auction in the future + let duration = 99u32; + let lease_period_index_start = 4u32; + assert_ok!(Auctions::new_auction(Origin::root(), duration, lease_period_index_start)); + + for n in 1 ..= max_bids { + // Increment block number + run_to_block(n * 10); + + Balances::make_free_balance_be(&(n * 10), n * 1_000); + + let (start, end) = match n { + 1 => (0, 0), + 2 => (0, 1), + 3 => (0, 2), + 4 => (0, 3), + 5 => (1, 1), + 6 => (1, 2), + 7 => (1, 3), + 8 => (2, 2), + 9 => (2, 3), + 10 => (3, 3), + _ => panic!("test not meant for this"), + }; + + // User 10 will bid directly for parachain 1 + assert_ok!(Auctions::bid( + Origin::signed(n * 10), + ParaId::from(n), + 1, // Auction Index + lease_period_index_start + start, // First Slot + lease_period_index_start + end, // Last slot + n * 900, // Amount + )); + } + + // All winner slots are filled by bids + for winner in &auctions::Winning::::get(0).unwrap() { + assert!(winner.is_some()); + } + + // Auction should be done + run_to_block(110); + + // Appropriate Paras should have won slots + // 900 + 4500 + 2x 8100 = 21,600 + // 900 + 4500 + 7200 + 9000 = 21,600 + assert_eq!( + slots::Leases::::get(ParaId::from(1)), + // -- 1 --- 2 --- 3 ---------- 4 ------ + vec![None, None, None, Some((10, 900))], + ); + assert_eq!( + slots::Leases::::get(ParaId::from(5)), + // -- 1 --- 2 --- 3 --- 4 ---------- 5 ------- + vec![None, None, None, None, Some((50, 4500))], + ); + // TODO: Is this right? + assert_eq!( + slots::Leases::::get(ParaId::from(9)), + // -- 1 --- 2 --- 3 --- 4 --- 5 ---------- 6 --------------- 7 ------- + vec![None, None, None, None, None, Some((90, 8100)), Some((90, 8100))], + ); + }); +} + +#[test] +fn competing_bids() { + // This test will verify that competing bids, from different sources will resolve appropriately. + new_test_ext().execute_with(|| { + assert!(System::block_number().is_one()); + // Start a new auction in the future + let duration = 99u32; + let lease_period_index_start = 4u32; + assert_ok!(Auctions::new_auction(Origin::root(), duration, lease_period_index_start)); + + // Create 3 paras and owners + for n in 1 ..= 3 { + Balances::make_free_balance_be(&n, 1_000); + let genesis_head = Registrar::worst_head_data(); + let validation_code = Registrar::worst_validation_code(); + assert_ok!(Registrar::register( + Origin::signed(n), + ParaId::from(n), + genesis_head, + validation_code, + )); + + // Create a crowdloan for each para + assert_ok!(Crowdloan::create( + Origin::signed(n), + ParaId::from(n), + 100_000, // Cap + lease_period_index_start + 2, // First Slot + lease_period_index_start + 3, // Last Slot + 200, // Block End, + None, + )); + } + + for n in 1 ..= 9 { + // Increment block number + run_to_block(n * 10); + + Balances::make_free_balance_be(&(n * 10), n * 1_000); + + let para = n % 3 + 1; + + if n % 2 == 0 { + // User 10 will bid directly for parachain 1 + assert_ok!(Auctions::bid( + Origin::signed(n * 10), + ParaId::from(para), + 1, // Auction Index + lease_period_index_start + 0, // First Slot + lease_period_index_start + 1, // Last slot + n * 900, // Amount + )); + } else { + // User 20 will be a contribute to crowdfund for parachain 2 + assert_ok!(Crowdloan::contribute( + Origin::signed(n * 10), + ParaId::from(para), + n + 900, + None, + )); + } + } + + // Auction should be done + run_to_block(110); + + // Appropriate Paras should have won slots + let crowdloan_2 = Crowdloan::fund_account_id(ParaId::from(2)); + assert_eq!( + slots::Leases::::get(ParaId::from(1)), + // -- 1 --- 2 --- 3 --- 4 --- 5 ------------- 6 ------------------------ 7 ------------- + vec![None, None, None, None, None, Some((crowdloan_2, 1812)), Some((crowdloan_2, 1812))], + ); + assert_eq!( + slots::Leases::::get(ParaId::from(3)), + // -- 1 --- 2 --- 3 ---------- 4 --------------- 5 ------- + vec![None, None, None, Some((80, 7200)), Some((80, 7200))], + ); + }); +} + +#[test] +fn basic_swap_works() { + // This test will test a swap between a parachain and parathread works successfully. + new_test_ext().execute_with(|| { + assert!(System::block_number().is_one()); // So events are emitted + // User 1 and 2 will own paras + Balances::make_free_balance_be(&1, 1_000); + Balances::make_free_balance_be(&2, 1_000); + // First register 2 parathreads with different data + assert_ok!(Registrar::register( + Origin::signed(1), + ParaId::from(1), + test_genesis_head(10), + test_validation_code(10), + )); + assert_ok!(Registrar::register( + Origin::signed(2), + ParaId::from(2), + test_genesis_head(20), + test_validation_code(20), + )); + + // Paras should be onboarding + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Onboarding)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Onboarding)); + + // Start a new auction in the future + let duration = 99u32; + let lease_period_index_start = 4u32; + assert_ok!(Auctions::new_auction(Origin::root(), duration, lease_period_index_start)); + + // 2 sessions later they are parathreads + run_to_session(2); + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Parathread)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Parathread)); + + // Open a crowdloan for Para 1 for slots 0-3 + assert_ok!(Crowdloan::create( + Origin::signed(1), + ParaId::from(1), + 1_000_000, // Cap + lease_period_index_start + 0, // First Slot + lease_period_index_start + 3, // Last Slot + 200, // Block End + None, + )); + // TODO: Check why this is the same for all paras + let crowdloan_account = Crowdloan::fund_account_id(ParaId::from(1)); + + // Bunch of contributions + let mut total = 0; + for i in 10 .. 20 { + Balances::make_free_balance_be(&i, 1_000); + assert_ok!(Crowdloan::contribute(Origin::signed(i), ParaId::from(1), 900 - i, None)); + total += 900 - i; + } + assert!(total > 0); + assert_eq!(Balances::free_balance(&crowdloan_account), total); + + // Go to end of auction where everyone won their slots + run_to_block(200); + + // Deposit is appropriately taken + // ----------------------------------------- para deposit --- crowdloan + assert_eq!(Balances::reserved_balance(&1), (500 + 10 * 2 * 1) + 100); + assert_eq!(Balances::reserved_balance(&2), 500 + 20 * 2 * 1); + assert_eq!(Balances::reserved_balance(&crowdloan_account), total); + // Crowdloan is appropriately set + assert!(Crowdloan::funds(ParaId::from(1)).is_some()); + assert!(Crowdloan::funds(ParaId::from(2)).is_none()); + + // New leases will start on block 400 + let lease_start_block = 400; + run_to_block(lease_start_block); + + // Slots are won by Para 1 + assert!(!Slots::lease(ParaId::from(1)).is_empty()); + assert!(Slots::lease(ParaId::from(2)).is_empty()); + + // 2 sessions later it is a parachain + run_to_block(lease_start_block + 20); + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Parachain)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Parathread)); + + // Initiate a swap + assert_ok!(Registrar::swap(para_origin(1).into(), ParaId::from(2))); + assert_ok!(Registrar::swap(para_origin(2).into(), ParaId::from(1))); + + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::DowngradingParachain)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::UpgradingParathread)); + + // 2 session later they have swapped + run_to_block(lease_start_block + 40); + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Parathread)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Parachain)); + + // Deregister parathread + assert_ok!(Registrar::deregister(para_origin(1).into(), ParaId::from(1))); + // Correct deposit is unreserved + assert_eq!(Balances::reserved_balance(&1), 100); // crowdloan deposit left over + assert_eq!(Balances::reserved_balance(&2), 500 + 20 * 2 * 1); + // Crowdloan ownership is swapped + assert!(Crowdloan::funds(ParaId::from(1)).is_none()); + assert!(Crowdloan::funds(ParaId::from(2)).is_some()); + // Slot is swapped + assert!(Slots::lease(ParaId::from(1)).is_empty()); + assert!(!Slots::lease(ParaId::from(2)).is_empty()); + + // Cant dissolve + assert_noop!(Crowdloan::dissolve(Origin::signed(1), ParaId::from(1)), CrowdloanError::::InvalidParaId); + assert_noop!(Crowdloan::dissolve(Origin::signed(2), ParaId::from(2)), CrowdloanError::::NotReadyToDissolve); + + // Go way in the future when the para is offboarded + run_to_block(lease_start_block + 1000); + + // Withdraw of contributions works + assert_eq!(Balances::free_balance(&crowdloan_account), total); + for i in 10 .. 20 { + assert_ok!(Crowdloan::withdraw(Origin::signed(i), i, ParaId::from(2))); + } + assert_eq!(Balances::free_balance(&crowdloan_account), 0); + + // Dissolve returns the balance of the person who put a deposit for crowdloan + assert_ok!(Crowdloan::dissolve(Origin::signed(2), ParaId::from(2))); + assert_eq!(Balances::reserved_balance(&1), 0); + assert_eq!(Balances::reserved_balance(&2), 500 + 20 * 2 * 1); + + // Final deregister sets everything back to the start + assert_ok!(Registrar::deregister(para_origin(2).into(), ParaId::from(2))); + assert_eq!(Balances::reserved_balance(&2), 0); + }) +} + +#[test] +fn crowdloan_ending_period_bid() { + new_test_ext().execute_with(|| { + assert!(System::block_number().is_one()); // So events are emitted + // User 1 and 2 will own paras + Balances::make_free_balance_be(&1, 1_000); + Balances::make_free_balance_be(&2, 1_000); + // First register 2 parathreads + assert_ok!(Registrar::register( + Origin::signed(1), + ParaId::from(1), + test_genesis_head(10), + test_validation_code(10), + )); + assert_ok!(Registrar::register( + Origin::signed(2), + ParaId::from(2), + test_genesis_head(20), + test_validation_code(20), + )); + + // Paras should be onboarding + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Onboarding)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Onboarding)); + + // Start a new auction in the future + let duration = 99u32; + let lease_period_index_start = 4u32; + assert_ok!(Auctions::new_auction(Origin::root(), duration, lease_period_index_start)); + + // 2 sessions later they are parathreads + run_to_session(2); + assert_eq!(Paras::lifecycle(ParaId::from(1)), Some(ParaLifecycle::Parathread)); + assert_eq!(Paras::lifecycle(ParaId::from(2)), Some(ParaLifecycle::Parathread)); + + // Open a crowdloan for Para 1 for slots 0-3 + assert_ok!(Crowdloan::create( + Origin::signed(1), + ParaId::from(1), + 1_000_000, // Cap + lease_period_index_start + 0, // First Slot + lease_period_index_start + 3, // Last Slot + 200, // Block End + None, + )); + // TODO: Check why this is the same for all paras + let crowdloan_account = Crowdloan::fund_account_id(ParaId::from(1)); + + // Bunch of contributions + let mut total = 0; + for i in 10 .. 20 { + Balances::make_free_balance_be(&i, 1_000); + assert_ok!(Crowdloan::contribute(Origin::signed(i), ParaId::from(1), 900 - i, None)); + total += 900 - i; + } + assert!(total > 0); + assert_eq!(Balances::free_balance(&crowdloan_account), total); + + // Bid for para 2 directly + Balances::make_free_balance_be(&2, 1_000); + assert_ok!(Auctions::bid( + Origin::signed(2), + ParaId::from(2), + 1, // Auction Index + lease_period_index_start + 0, // First Slot + lease_period_index_start + 1, // Last slot + 900, // Amount + )); + + // Go to beginning of ending period + run_to_block(100); + + assert_eq!(Auctions::is_ending(100), Some(0)); + + assert_eq!(Auctions::winning(0), Some( + [ + None, // 0-0 + Some((2, ParaId::from(2), 900)), // 0-1 + None, // 0-2 + Some((crowdloan_account, ParaId::from(1), total)), // 0-3 + None, // 1-1 + None, // 1-2 + None, // 1-3 + None, // 2-2 + None, // 2-3 + None, // 3-3 + ] + )); + + run_to_block(101); + + Balances::make_free_balance_be(&1234, 1_000); + assert_ok!(Crowdloan::contribute(Origin::signed(1234), ParaId::from(1), 900, None)); + + // Data propagates correctly + run_to_block(102); + assert_eq!(Auctions::winning(2), Some( + [ + None, // 0-0 + Some((2, ParaId::from(2), 900)), // 0-1 + None, // 0-2 + Some((crowdloan_account, ParaId::from(1), total + 900)), // 0-3 + None, // 1-1 + None, // 1-2 + None, // 1-3 + None, // 2-2 + None, // 2-3 + None, // 3-3 + ] + )); + }) +} diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 5b48ae9aef90..a160095d4c24 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -21,11 +21,17 @@ pub mod claims; pub mod slot_range; pub mod slots; +pub mod auctions; pub mod crowdloan; pub mod purchase; pub mod impls; pub mod paras_sudo_wrapper; pub mod paras_registrar; +pub mod traits; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod integration_tests; pub mod xcm_sender; use primitives::v1::{BlockNumber, ValidatorId, AssignmentId}; diff --git a/runtime/common/src/mock.rs b/runtime/common/src/mock.rs new file mode 100644 index 000000000000..8e23d707e73c --- /dev/null +++ b/runtime/common/src/mock.rs @@ -0,0 +1,184 @@ +// Copyright 2019-2021 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! Mocking utilities for testing. + +use std::{cell::RefCell, collections::HashMap}; +use parity_scale_codec::{Encode, Decode}; +use sp_runtime::traits::SaturatedConversion; +use frame_support::dispatch::DispatchResult; +use primitives::v1::{HeadData, ValidationCode, Id as ParaId}; +use crate::traits::Registrar; + +thread_local! { + static OPERATIONS: RefCell> = RefCell::new(Vec::new()); + static PARACHAINS: RefCell> = RefCell::new(Vec::new()); + static PARATHREADS: RefCell> = RefCell::new(Vec::new()); + static MANAGERS: RefCell>> = RefCell::new(HashMap::new()); +} + +pub struct TestRegistrar(sp_std::marker::PhantomData); + +impl Registrar for TestRegistrar { + type AccountId = T::AccountId; + + fn manager_of(id: ParaId) -> Option { + MANAGERS.with(|x| x.borrow().get(&id).and_then(|v| T::AccountId::decode(&mut &v[..]).ok())) + } + + fn parachains() -> Vec { + PARACHAINS.with(|x| x.borrow().clone()) + } + + fn is_parathread(id: ParaId) -> bool { + PARATHREADS.with(|x| x.borrow().binary_search(&id).is_ok()) + } + + fn register( + manager: Self::AccountId, + id: ParaId, + _genesis_head: HeadData, + _validation_code: ValidationCode, + ) -> DispatchResult { + // Should not be parachain. + PARACHAINS.with(|x| { + let parachains = x.borrow_mut(); + match parachains.binary_search(&id) { + Ok(_) => panic!("Already Parachain"), + Err(_) => {}, + } + }); + // Should not be parathread, then make it. + PARATHREADS.with(|x| { + let mut parathreads = x.borrow_mut(); + match parathreads.binary_search(&id) { + Ok(_) => panic!("Already Parathread"), + Err(i) => parathreads.insert(i, id), + } + }); + MANAGERS.with(|x| x.borrow_mut().insert(id, manager.encode())); + Ok(()) + } + + fn deregister(id: ParaId) -> DispatchResult { + // Should not be parachain. + PARACHAINS.with(|x| { + let mut parachains = x.borrow_mut(); + match parachains.binary_search(&id) { + Ok(i) => { + parachains.remove(i); + }, + Err(_) => {}, + } + }); + // Remove from parathread. + PARATHREADS.with(|x| { + let mut parathreads = x.borrow_mut(); + match parathreads.binary_search(&id) { + Ok(i) => { + parathreads.remove(i); + }, + Err(_) => {}, + } + }); + MANAGERS.with(|x| x.borrow_mut().remove(&id)); + Ok(()) + } + + fn make_parachain(id: ParaId) -> DispatchResult { + OPERATIONS.with(|x| x.borrow_mut().push((id, frame_system::Module::::block_number().saturated_into(), true))); + PARATHREADS.with(|x| { + let mut parathreads = x.borrow_mut(); + match parathreads.binary_search(&id) { + Ok(i) => { + parathreads.remove(i); + }, + Err(_) => panic!("not parathread, so cannot `make_parachain`"), + } + }); + PARACHAINS.with(|x| { + let mut parachains = x.borrow_mut(); + match parachains.binary_search(&id) { + Ok(_) => {}, + Err(i) => parachains.insert(i, id), + } + }); + Ok(()) + } + fn make_parathread(id: ParaId) -> DispatchResult { + OPERATIONS.with(|x| x.borrow_mut().push((id, frame_system::Module::::block_number().saturated_into(), false))); + PARACHAINS.with(|x| { + let mut parachains = x.borrow_mut(); + match parachains.binary_search(&id) { + Ok(i) => { + parachains.remove(i); + }, + Err(_) => panic!("not parachain, so cannot `make_parathread`"), + } + }); + PARATHREADS.with(|x| { + let mut parathreads = x.borrow_mut(); + match parathreads.binary_search(&id) { + Ok(_) => {}, + Err(i) => parathreads.insert(i, id), + } + }); + Ok(()) + } + + #[cfg(test)] + fn worst_head_data() -> HeadData { + vec![0u8; 1000].into() + } + + #[cfg(test)] + fn worst_validation_code() -> ValidationCode { + let mut validation_code = vec![0u8; 1000]; + // Replace first bytes of code with "WASM_MAGIC" to pass validation test. + let _ = validation_code.splice( + ..crate::WASM_MAGIC.len(), + crate::WASM_MAGIC.iter().cloned(), + ).collect::>(); + validation_code.into() + } + + #[cfg(test)] + fn execute_pending_transitions() {} +} + +impl TestRegistrar { + pub fn operations() -> Vec<(ParaId, T::BlockNumber, bool)> { + OPERATIONS.with(|x| x.borrow().iter().map(|(p, b, c)| (*p, (*b).into(), *c)).collect::>()) + } + + #[allow(dead_code)] + pub fn parachains() -> Vec { + PARACHAINS.with(|x| x.borrow().clone()) + } + + #[allow(dead_code)] + pub fn parathreads() -> Vec { + PARATHREADS.with(|x| x.borrow().clone()) + } + + #[allow(dead_code)] + pub fn clear_storage() { + OPERATIONS.with(|x| x.borrow_mut().clear()); + PARACHAINS.with(|x| x.borrow_mut().clear()); + PARATHREADS.with(|x| x.borrow_mut().clear()); + MANAGERS.with(|x| x.borrow_mut().clear()); + } +} diff --git a/runtime/common/src/paras_registrar.rs b/runtime/common/src/paras_registrar.rs index 11bad845f522..a2d4b92522f6 100644 --- a/runtime/common/src/paras_registrar.rs +++ b/runtime/common/src/paras_registrar.rs @@ -20,9 +20,10 @@ use crate::WASM_MAGIC; use sp_std::{prelude::*, result}; use frame_support::{ - decl_storage, decl_module, decl_error, ensure, + decl_storage, decl_module, decl_error, decl_event, ensure, dispatch::DispatchResult, traits::{Get, Currency, ReservableCurrency}, + pallet_prelude::Weight, }; use frame_system::{self, ensure_root, ensure_signed}; use primitives::v1::{ @@ -33,15 +34,40 @@ use runtime_parachains::{ self, ParaGenesisArgs, }, - dmp, ump, hrmp, ensure_parachain, - Origin, + Origin, ParaLifecycle, }; +use crate::traits::{Registrar, OnSwap}; +use parity_scale_codec::{Encode, Decode}; +use sp_runtime::{RuntimeDebug, traits::Saturating}; + +#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug)] +pub struct ParaInfo { + pub(crate) manager: Account, + deposit: Balance, +} + type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; -pub trait Config: paras::Config + dmp::Config + ump::Config + hrmp::Config { +pub trait WeightInfo { + fn register() -> Weight; + fn deregister() -> Weight; + fn swap() -> Weight; +} + +pub struct TestWeightInfo; +impl WeightInfo for TestWeightInfo { + fn register() -> Weight { 0 } + fn deregister() -> Weight { 0 } + fn swap() -> Weight { 0 } +} + +pub trait Config: paras::Config { + /// The overarching event type. + type Event: From> + Into<::Event>; + /// The aggregated origin type must support the `parachains` origin. We require that we can /// infallibly convert between this origin and the system origin, but in reality, they're the /// same type, we just can't express that to the Rust type system without writing a `where` @@ -52,44 +78,73 @@ pub trait Config: paras::Config + dmp::Config + ump::Config + hrmp::Config { /// The system's currency for parathread payment. type Currency: ReservableCurrency; + /// Runtime hook for when a parachain and parathread swap. + type OnSwap: crate::traits::OnSwap; + /// The deposit to be paid to run a parathread. - type ParathreadDeposit: Get>; + /// This should include the cost for storing the genesis head and validation code. + type ParaDeposit: Get>; + + /// The deposit to be paid per byte stored on chain. + type DataDepositPerByte: Get>; + + /// The maximum size for the validation code. + type MaxCodeSize: Get; + + /// The maximum size for the head data. + type MaxHeadSize: Get; + + /// Weight Information for the Extrinsics in the Pallet + type WeightInfo: WeightInfo; } decl_storage! { trait Store for Module as Registrar { - /// Whether parathreads are enabled or not. - ParathreadsRegistrationEnabled: bool; - /// Pending swap operations. PendingSwap: map hasher(twox_64_concat) ParaId => Option; - /// Map of all registered parathreads/chains. - Paras get(fn paras): map hasher(twox_64_concat) ParaId => Option; + /// Amount held on deposit for each para and the original depositor. + /// + /// The given account ID is responsible for registering the code and initial head data, but may only do + /// so if it isn't yet registered. (After that, it's up to governance to do so.) + pub Paras: map hasher(twox_64_concat) ParaId => Option>>; + } +} - /// Users who have paid a parathread's deposit. - Debtors: map hasher(twox_64_concat) ParaId => T::AccountId; +decl_event! { + pub enum Event where + AccountId = ::AccountId, + ParaId = ParaId, + { + Registered(ParaId, AccountId), + Deregistered(ParaId), } } decl_error! { pub enum Error for Module { - /// Parachain already exists. - ParaAlreadyExists, - /// Invalid parachain ID. - InvalidChainId, - /// Invalid parathread ID. - InvalidThreadId, + /// The ID is not registered. + NotRegistered, + /// The ID is already registered. + AlreadyRegistered, + /// The caller is not the owner of this Id. + NotOwner, /// Invalid para code size. CodeTooLarge, /// Invalid para head data size. HeadDataTooLarge, - /// Parathreads registration is disabled. - ParathreadsRegistrationDisabled, /// The validation code provided doesn't start with the Wasm file magic string. DefinitelyNotWasm, + /// Para is not a Parachain. + NotParachain, + /// Para is not a Parathread. + NotParathread, /// Cannot deregister para CannotDeregister, + /// Cannot schedule downgrade of parachain to parathread + CannotDowngrade, + /// Cannot schedule upgrade of parathread to parachain + CannotUpgrade, } } @@ -97,83 +152,48 @@ decl_module! { pub struct Module for enum Call where origin: ::Origin { type Error = Error; - /// Register a parathread with given code for immediate use. + const ParaDeposit: BalanceOf = T::ParaDeposit::get(); + const DataDepositPerByte: BalanceOf = T::DataDepositPerByte::get(); + const MaxCodeSize: u32 = T::MaxCodeSize::get(); + const MaxHeadSize: u32 = T::MaxHeadSize::get(); + + fn deposit_event() = default; + + /// Register a Para Id on the relay chain. + /// + /// This function will queue the new Para Id to be a parathread. + /// Using the Slots pallet, a parathread can then be upgraded to get a + /// parachain slot. /// - /// Must be sent from a Signed origin that is able to have `ParathreadDeposit` reserved. - /// `genesis_head` and `validation_code` are used to initalize the parathread's state. - #[weight = 0] - fn register_parathread( + /// This function must be called by a signed origin. + /// + /// The origin must pay a deposit for the registration information, + /// including the genesis information and validation code. + #[weight = T::WeightInfo::register()] + pub fn register( origin, id: ParaId, genesis_head: HeadData, validation_code: ValidationCode, ) -> DispatchResult { let who = ensure_signed(origin)?; - - ensure!(ParathreadsRegistrationEnabled::get(), Error::::ParathreadsRegistrationDisabled); - ensure!(validation_code.0.starts_with(WASM_MAGIC), Error::::DefinitelyNotWasm); - - ensure!(!Paras::contains_key(id), Error::::ParaAlreadyExists); - - let genesis = ParaGenesisArgs { - genesis_head, - validation_code, - parachain: false, - }; - ensure!(paras::Module::::can_schedule_para_initialize(&id, &genesis), Error::::ParaAlreadyExists); - ::Currency::reserve(&who, T::ParathreadDeposit::get())?; - - >::insert(id, who); - Paras::insert(id, false); - // Checked this shouldn't fail above. - let _ = runtime_parachains::schedule_para_initialize::(id, genesis); - - Ok(()) + Self::do_register(who, id, genesis_head, validation_code) } - /// Deregister a parathread and retreive the deposit. - /// - /// Must be sent from a `Parachain` origin which is currently a parathread. + /// Deregister a Para Id, freeing all data and returning any deposit. /// - /// Ensure that before calling this that any funds you want emptied from the parathread's - /// account is moved out; after this it will be impossible to retreive them (without - /// governance intervention). - #[weight = 0] - fn deregister_parathread(origin) -> DispatchResult { - let id = ensure_parachain(::Origin::from(origin))?; - - ensure!(ParathreadsRegistrationEnabled::get(), Error::::ParathreadsRegistrationDisabled); - - let is_parachain = Paras::get(id).ok_or(Error::::InvalidChainId)?; - - ensure!(!is_parachain, Error::::InvalidThreadId); - - runtime_parachains::schedule_para_cleanup::(id).map_err(|_| Error::::CannotDeregister)?; - - let debtor = >::take(id); - let _ = ::Currency::unreserve(&debtor, T::ParathreadDeposit::get()); - Paras::remove(&id); - PendingSwap::remove(&id); - - Ok(()) - } - - #[weight = 0] - fn enable_parathread_registration(origin) -> DispatchResult { - ensure_root(origin)?; - - ParathreadsRegistrationEnabled::put(true); - - Ok(()) - } - - #[weight = 0] - fn disable_parathread_registration(origin) -> DispatchResult { - ensure_root(origin)?; - - ParathreadsRegistrationEnabled::put(false); + /// The caller must be the para itself or Root and the para must be a parathread. + #[weight = T::WeightInfo::deregister()] + pub fn deregister(origin, id: ParaId) -> DispatchResult { + match ensure_root(origin.clone()) { + Ok(_) => {}, + Err(_) => { + let caller_id = ensure_parachain(::Origin::from(origin))?; + ensure!(caller_id == id, Error::::NotOwner); + }, + }; - Ok(()) + Self::do_deregister(id) } /// Swap a parachain with another parachain or parathread. The origin must be a `Parachain`. @@ -184,25 +204,34 @@ decl_module! { /// `ParaId` to be a long-term identifier of a notional "parachain". However, their /// scheduling info (i.e. whether they're a parathread or parachain), auction information /// and the auction deposit are switched. - #[weight = 0] - fn swap(origin, other: ParaId) { + #[weight = T::WeightInfo::swap()] + pub fn swap(origin, other: ParaId) { let id = ensure_parachain(::Origin::from(origin))?; - if PendingSwap::get(other) == Some(id) { - // Remove intention to swap. - PendingSwap::remove(other); - - Paras::mutate(id, |i| - Paras::mutate(other, |j| - sp_std::mem::swap(i, j) - ) - ); - - >::mutate(id, |i| - >::mutate(other, |j| - sp_std::mem::swap(i, j) - ) - ); + if let Some(other_lifecycle) = paras::Module::::lifecycle(other) { + if let Some(id_lifecycle) = paras::Module::::lifecycle(id) { + // identify which is a parachain and which is a parathread + if id_lifecycle.is_parachain() && other_lifecycle.is_parathread() { + // We check that both paras are in an appropriate lifecycle for a swap, + // so these should never fail. + let res1 = runtime_parachains::schedule_parachain_downgrade::(id); + debug_assert!(res1.is_ok()); + let res2 = runtime_parachains::schedule_parathread_upgrade::(other); + debug_assert!(res2.is_ok()); + T::OnSwap::on_swap(id, other); + } else if id_lifecycle.is_parathread() && other_lifecycle.is_parachain() { + // We check that both paras are in an appropriate lifecycle for a swap, + // so these should never fail. + let res1 = runtime_parachains::schedule_parachain_downgrade::(other); + debug_assert!(res1.is_ok()); + let res2 = runtime_parachains::schedule_parathread_upgrade::(id); + debug_assert!(res2.is_ok()); + T::OnSwap::on_swap(id, other); + } + + PendingSwap::remove(other); + } + } } else { PendingSwap::insert(id, other); } @@ -210,41 +239,161 @@ decl_module! { } } -impl Module { - /// Register a parachain with given code. Must be called by root. - /// Fails if given ID is already used. - pub fn register_parachain( +impl Registrar for Module { + type AccountId = T::AccountId; + + /// Return the manager `AccountId` of a para if one exists. + fn manager_of(id: ParaId) -> Option { + Some(Paras::::get(id)?.manager) + } + + // All parachains. Ordered ascending by ParaId. Parathreads are not included. + fn parachains() -> Vec { + paras::Module::::parachains() + } + + // Return if a para is a parathread + fn is_parathread(id: ParaId) -> bool { + paras::Module::::is_parathread(id) + } + + // Return if a para is a parachain + fn is_parachain(id: ParaId) -> bool { + paras::Module::::is_parachain(id) + } + + // Register a Para ID under control of `manager`. + fn register( + manager: T::AccountId, id: ParaId, genesis_head: HeadData, validation_code: ValidationCode, ) -> DispatchResult { - ensure!(!Paras::contains_key(id), Error::::ParaAlreadyExists); - ensure!(validation_code.0.starts_with(WASM_MAGIC), Error::::DefinitelyNotWasm); + Self::do_register(manager, id, genesis_head, validation_code) + } - let genesis = ParaGenesisArgs { - genesis_head, - validation_code, - parachain: true, - }; + // Deregister a Para ID, free any data, and return any deposits. + fn deregister(id: ParaId) -> DispatchResult { + Self::do_deregister(id) + } - runtime_parachains::schedule_para_initialize::(id, genesis).map_err(|_| Error::::ParaAlreadyExists)?; + // Upgrade a registered parathread into a parachain. + fn make_parachain(id: ParaId) -> DispatchResult { + // Para backend should think this is a parathread... + ensure!(paras::Module::::lifecycle(id) == Some(ParaLifecycle::Parathread), Error::::NotParathread); + runtime_parachains::schedule_parathread_upgrade::(id).map_err(|_| Error::::CannotUpgrade)?; + Ok(()) + } - Paras::insert(id, true); + // Downgrade a registered para into a parathread. + fn make_parathread(id: ParaId) -> DispatchResult { + // Para backend should think this is a parachain... + ensure!(paras::Module::::lifecycle(id) == Some(ParaLifecycle::Parachain), Error::::NotParachain); + runtime_parachains::schedule_parachain_downgrade::(id).map_err(|_| Error::::CannotDowngrade)?; Ok(()) } - /// Deregister a parachain with the given ID. Must be called by root. - pub fn deregister_parachain(id: ParaId) -> DispatchResult { - let is_parachain = Paras::get(id).ok_or(Error::::InvalidChainId)?; + #[cfg(any(feature = "runtime-benchmarks", test))] + fn worst_head_data() -> HeadData { + // TODO: Figure a way to allow bigger head data in benchmarks? + let max_head_size = (T::MaxHeadSize::get()).min(1 * 1024 * 1024); + vec![0u8; max_head_size as usize].into() + } - ensure!(is_parachain, Error::::InvalidChainId); + #[cfg(any(feature = "runtime-benchmarks", test))] + fn worst_validation_code() -> ValidationCode { + // TODO: Figure a way to allow bigger wasm in benchmarks? + let max_code_size = (T::MaxCodeSize::get()).min(4 * 1024 * 1024); + let mut validation_code = vec![0u8; max_code_size as usize]; + // Replace first bytes of code with "WASM_MAGIC" to pass validation test. + let _ = validation_code.splice( + ..crate::WASM_MAGIC.len(), + crate::WASM_MAGIC.iter().cloned(), + ).collect::>(); + validation_code.into() + } + #[cfg(any(feature = "runtime-benchmarks", test))] + fn execute_pending_transitions() { + use runtime_parachains::shared; + shared::Module::::set_session_index( + shared::Module::::scheduled_session() + ); + paras::Module::::test_on_new_session(); + } +} + +impl Module { + /// Attempt to register a new Para Id under management of `who` in the + /// system with the given information. + fn do_register( + who: T::AccountId, + id: ParaId, + genesis_head: HeadData, + validation_code: ValidationCode, + ) -> DispatchResult { + ensure!(!Paras::::contains_key(id), Error::::AlreadyRegistered); + ensure!(paras::Module::::lifecycle(id).is_none(), Error::::AlreadyRegistered); + let (genesis, deposit) = Self::validate_onboarding_data( + genesis_head, + validation_code, + false + )?; + + ::Currency::reserve(&who, deposit)?; + let info = ParaInfo { + manager: who.clone(), + deposit: deposit, + }; + + Paras::::insert(id, info); + // We check above that para has no lifecycle, so this should not fail. + let res = runtime_parachains::schedule_para_initialize::(id, genesis); + debug_assert!(res.is_ok()); + Self::deposit_event(RawEvent::Registered(id, who)); + Ok(()) + } + + /// Deregister a Para Id, freeing all data returning any deposit. + fn do_deregister(id: ParaId) -> DispatchResult { + ensure!(paras::Module::::lifecycle(id) == Some(ParaLifecycle::Parathread), Error::::NotParathread); runtime_parachains::schedule_para_cleanup::(id).map_err(|_| Error::::CannotDeregister)?; - Paras::remove(&id); - PendingSwap::remove(&id); + if let Some(info) = Paras::::take(&id) { + ::Currency::unreserve(&info.manager, info.deposit); + } + + PendingSwap::remove(id); + Self::deposit_event(RawEvent::Deregistered(id)); Ok(()) } + + /// Verifies the onboarding data is valid for a para. + /// + /// Returns `ParaGenesisArgs` and the deposit needed for the data. + fn validate_onboarding_data( + genesis_head: HeadData, + validation_code: ValidationCode, + parachain: bool, + ) -> Result<(ParaGenesisArgs, BalanceOf), sp_runtime::DispatchError> { + ensure!(validation_code.0.len() <= T::MaxCodeSize::get() as usize, Error::::CodeTooLarge); + ensure!(genesis_head.0.len() <= T::MaxHeadSize::get() as usize, Error::::HeadDataTooLarge); + ensure!(validation_code.0.starts_with(WASM_MAGIC), Error::::DefinitelyNotWasm); + + let per_byte_fee = T::DataDepositPerByte::get(); + let deposit = T::ParaDeposit::get() + .saturating_add( + per_byte_fee.saturating_mul((genesis_head.0.len() as u32).into()) + ).saturating_add( + per_byte_fee.saturating_mul((validation_code.0.len() as u32).into()) + ); + + Ok((ParaGenesisArgs { + genesis_head, + validation_code, + parachain, + }, deposit)) + } } #[cfg(test)] @@ -254,24 +403,19 @@ mod tests { use sp_core::H256; use sp_runtime::{ traits::{ - BlakeTwo256, IdentityLookup, Extrinsic as ExtrinsicT, - }, testing::{UintAuthorityId, TestXt}, Perbill, curve::PiecewiseLinear, - }; - use primitives::v1::{ - Balance, BlockNumber, Header, Signature, AuthorityDiscoveryId, ValidatorIndex, + BlakeTwo256, IdentityLookup, + }, Perbill, }; + use primitives::v1::{Balance, BlockNumber, Header}; use frame_system::limits; use frame_support::{ traits::{OnInitialize, OnFinalize}, + error::BadOrigin, assert_ok, assert_noop, parameter_types, }; - use frame_support_test::TestRandomness; - use keyring::Sr25519Keyring; - use runtime_parachains::{ - initializer, configuration, inclusion, session_info, scheduler, dmp, ump, hrmp, shared, - ParaLifecycle, - }; - use frame_support::traits::OneSessionHandler; + use runtime_parachains::{configuration, shared}; + use pallet_balances::Error as BalancesError; + use crate::traits::Registrar as RegistrarTrait; use crate::paras_registrar; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -286,27 +430,10 @@ mod tests { System: frame_system::{Module, Call, Config, Storage, Event}, Balances: pallet_balances::{Module, Call, Storage, Config, Event}, Parachains: paras::{Module, Origin, Call, Storage, Config}, - Shared: shared::{Module, Call, Storage}, - Inclusion: inclusion::{Module, Call, Storage, Event}, - Registrar: paras_registrar::{Module, Call, Storage}, - Staking: pallet_staking::{Module, Call, Config, Storage, Event, ValidateUnsigned}, - Session: pallet_session::{Module, Call, Storage, Event, Config}, - Initializer: initializer::{Module, Call, Storage}, - Hrmp: hrmp::{Module, Call, Storage, Event}, + Registrar: paras_registrar::{Module, Call, Storage, Event}, } ); - pallet_staking_reward_curve::build! { - const REWARD_CURVE: PiecewiseLinear<'static> = curve!( - min_inflation: 0_025_000, - max_inflation: 0_100_000, - ideal_stake: 0_500_000, - falloff: 0_050_000, - max_piece_count: 40, - test_precision: 0_005_000, - ); - } - const NORMAL_RATIO: Perbill = Perbill::from_percent(75); parameter_types! { pub const BlockHashCount: u32 = 250; @@ -341,13 +468,6 @@ mod tests { type SS58Prefix = (); } - impl frame_system::offchain::SendTransactionTypes for Test where - Call: From, - { - type OverarchingCall = Call; - type Extrinsic = TestXt; - } - parameter_types! { pub const ExistentialDeposit: Balance = 1; } @@ -362,385 +482,357 @@ mod tests { type WeightInfo = (); } - parameter_types!{ - pub const SlashDeferDuration: pallet_staking::EraIndex = 7; - pub const AttestationPeriod: BlockNumber = 100; - pub const MinimumPeriod: u64 = 3; - pub const SessionsPerEra: sp_staking::SessionIndex = 6; - pub const BondingDuration: pallet_staking::EraIndex = 28; - pub const MaxNominatorRewardedPerValidator: u32 = 64; - } - - parameter_types! { - pub const Period: BlockNumber = 3; - pub const Offset: BlockNumber = 0; - pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(17); - pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; - } - - impl pallet_session::Config for Test { - type SessionManager = (); - type Keys = UintAuthorityId; - type ShouldEndSession = pallet_session::PeriodicSessions; - type NextSessionRotation = pallet_session::PeriodicSessions; - type SessionHandler = pallet_session::TestSessionHandler; - type Event = Event; - type ValidatorId = u64; - type ValidatorIdOf = (); - type DisabledValidatorsThreshold = DisabledValidatorsThreshold; - type WeightInfo = (); - } - - parameter_types! { - pub const MaxHeadDataSize: u32 = 100; - pub const MaxCodeSize: u32 = 100; - - pub const ValidationUpgradeFrequency: BlockNumber = 10; - pub const ValidationUpgradeDelay: BlockNumber = 2; - pub const SlashPeriod: BlockNumber = 50; - pub const ElectionLookahead: BlockNumber = 0; - pub const StakingUnsignedPriority: u64 = u64::max_value() / 2; - } - - impl sp_election_providers::onchain::Config for Test { - type AccountId = ::AccountId; - type BlockNumber = ::BlockNumber; - type Accuracy = sp_runtime::Perbill; - type DataProvider = pallet_staking::Module; - } - - impl pallet_staking::Config for Test { - type RewardRemainder = (); - type CurrencyToVote = frame_support::traits::SaturatingCurrencyToVote; - type Event = Event; - type Currency = pallet_balances::Module; - type Slash = (); - type Reward = (); - type SessionsPerEra = SessionsPerEra; - type BondingDuration = BondingDuration; - type SlashDeferDuration = SlashDeferDuration; - type SlashCancelOrigin = frame_system::EnsureRoot; - type SessionInterface = Self; - type UnixTime = pallet_timestamp::Module; - type RewardCurve = RewardCurve; - type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator; - type NextNewSession = Session; - type ElectionLookahead = ElectionLookahead; - type Call = Call; - type UnsignedPriority = StakingUnsignedPriority; - type MaxIterations = (); - type MinSolutionScoreBump = (); - type OffchainSolutionWeightLimit = (); - type ElectionProvider = sp_election_providers::onchain::OnChainSequentialPhragmen; - type WeightInfo = (); - } - - impl pallet_timestamp::Config for Test { - type Moment = u64; - type OnTimestampSet = (); - type MinimumPeriod = MinimumPeriod; - type WeightInfo = (); - } - impl shared::Config for Test {} - impl dmp::Config for Test {} - - impl ump::Config for Test { - type UmpSink = (); - } - - impl hrmp::Config for Test { - type Event = Event; - type Origin = Origin; - type Currency = pallet_balances::Module; - } - - impl pallet_session::historical::Config for Test { - type FullIdentification = pallet_staking::Exposure; - type FullIdentificationOf = pallet_staking::ExposureOf; - } - - // This is needed for a custom `AccountId` type which is `u64` in testing here. - pub mod test_keys { - use sp_core::crypto::KeyTypeId; - - pub const KEY_TYPE: KeyTypeId = KeyTypeId(*b"test"); - - mod app { - use super::super::Shared; - use sp_application_crypto::{app_crypto, sr25519}; - - app_crypto!(sr25519, super::KEY_TYPE); - - impl sp_runtime::traits::IdentifyAccount for Public { - type AccountId = u64; - - fn into_account(self) -> Self::AccountId { - let id = self.0.clone().into(); - Shared::active_validator_keys().iter().position(|b| *b == id).unwrap() as u64 - } - } - } - - pub type ReporterId = app::Public; - } - impl paras::Config for Test { type Origin = Origin; } impl configuration::Config for Test { } - pub struct TestRewardValidators; - - impl inclusion::RewardValidators for TestRewardValidators { - fn reward_backing(_: impl IntoIterator) { } - fn reward_bitfields(_: impl IntoIterator) { } - } - - impl inclusion::Config for Test { - type Event = Event; - type RewardValidators = TestRewardValidators; - } - - impl session_info::AuthorityDiscoveryConfig for Test { - fn authorities() -> Vec { - Vec::new() - } - } - - impl session_info::Config for Test { } - - impl initializer::Config for Test { - type Randomness = TestRandomness; - } - - impl scheduler::Config for Test { } - - type Extrinsic = TestXt; - - impl frame_system::offchain::CreateSignedTransaction for Test where - Call: From, - { - fn create_transaction>( - call: Call, - _public: test_keys::ReporterId, - _account: ::AccountId, - nonce: ::Index, - ) -> Option<(Call, ::SignaturePayload)> { - Some((call, (nonce, ()))) - } - } - - impl frame_system::offchain::SigningTypes for Test { - type Public = test_keys::ReporterId; - type Signature = Signature; - } - parameter_types! { - pub const ParathreadDeposit: Balance = 10; + pub const ParaDeposit: Balance = 10; + pub const DataDepositPerByte: Balance = 1; pub const QueueSize: usize = 2; pub const MaxRetries: u32 = 3; + pub const MaxCodeSize: u32 = 100; + pub const MaxHeadSize: u32 = 100; } impl Config for Test { + type Event = Event; type Origin = Origin; - type Currency = pallet_balances::Module; - type ParathreadDeposit = ParathreadDeposit; + type Currency = Balances; + type OnSwap = (); + type ParaDeposit = ParaDeposit; + type DataDepositPerByte = DataDepositPerByte; + type MaxCodeSize = MaxCodeSize; + type MaxHeadSize = MaxHeadSize; + type WeightInfo = TestWeightInfo; } - fn new_test_ext() -> TestExternalities { + pub fn new_test_ext() -> TestExternalities { let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - let authority_keys = [ - Sr25519Keyring::Alice, - Sr25519Keyring::Bob, - Sr25519Keyring::Charlie, - Sr25519Keyring::Dave, - Sr25519Keyring::Eve, - Sr25519Keyring::Ferdie, - Sr25519Keyring::One, - Sr25519Keyring::Two, - ]; - - let balances: Vec<_> = (0..authority_keys.len()).map(|i| (i as u64, 10_000_000)).collect(); - pallet_balances::GenesisConfig:: { - balances, - }.assimilate_storage(&mut t).unwrap(); - - // stashes are the index. - let session_keys: Vec<_> = authority_keys.iter().enumerate() - .map(|(i, _k)| (i as u64, i as u64, UintAuthorityId(i as u64))) - .collect(); - - pallet_session::GenesisConfig:: { - keys: session_keys, + balances: vec![(1, 10_000_000), (2, 10_000_000)], }.assimilate_storage(&mut t).unwrap(); t.into() } + const BLOCKS_PER_SESSION: u32 = 3; + fn run_to_block(n: BlockNumber) { // NOTE that this function only simulates modules of interest. Depending on new module may // require adding it here. - println!("Running until block {}", n); + assert!(System::block_number() < n); while System::block_number() < n { let b = System::block_number(); if System::block_number() > 1 { - println!("Finalizing {}", System::block_number()); System::on_finalize(System::block_number()); - Initializer::on_finalize(System::block_number()); } // Session change every 3 blocks. - if (b + 1) % Period::get() == 0 { - println!("New session at {}", System::block_number()); - Initializer::on_new_session( - false, - Vec::new().into_iter(), - Vec::new().into_iter(), + if (b + 1) % BLOCKS_PER_SESSION == 0 { + shared::Module::::set_session_index( + shared::Module::::session_index() + 1 ); + Parachains::test_on_new_session(); } System::set_block_number(b + 1); - println!("Initializing {}", System::block_number()); System::on_initialize(System::block_number()); - Session::on_initialize(System::block_number()); - Initializer::on_initialize(System::block_number()); } } + fn run_to_session(n: BlockNumber) { + let block_number = n * BLOCKS_PER_SESSION; + run_to_block(block_number); + } + + fn test_genesis_head(size: usize) -> HeadData { + HeadData(vec![0u8; size]) + } + + fn test_validation_code(size: usize) -> ValidationCode { + let mut validation_code = vec![0u8; size as usize]; + // Replace first bytes of code with "WASM_MAGIC" to pass validation test. + let _ = validation_code.splice( + ..crate::WASM_MAGIC.len(), + crate::WASM_MAGIC.iter().cloned(), + ).collect::>(); + ValidationCode(validation_code) + } + + fn para_origin(id: ParaId) -> Origin { + runtime_parachains::Origin::Parachain(id).into() + } + #[test] fn basic_setup_works() { new_test_ext().execute_with(|| { assert_eq!(PendingSwap::get(&ParaId::from(0u32)), None); - assert_eq!(Paras::get(&ParaId::from(0u32)), None); + assert_eq!(Paras::::get(&ParaId::from(0u32)), None); }); } #[test] - fn register_deregister_chain_works() { + fn end_to_end_scenario_works() { new_test_ext().execute_with(|| { run_to_block(1); - - assert_ok!(Registrar::enable_parathread_registration( + // 32 is not yet registered + assert!(!Parachains::is_parathread(32.into())); + // We register the Para ID + assert_ok!(Registrar::register( + Origin::signed(1), + 32.into(), + test_genesis_head(32), + test_validation_code(32), + )); + run_to_session(2); + // It is now a parathread. + assert!(Parachains::is_parathread(32.into())); + assert!(!Parachains::is_parachain(32.into())); + // Some other external process will elevate parathread to parachain + assert_ok!(Registrar::make_parachain(32.into())); + run_to_session(4); + // It is now a parachain. + assert!(!Parachains::is_parathread(32.into())); + assert!(Parachains::is_parachain(32.into())); + // Turn it back into a parathread + assert_ok!(Registrar::make_parathread(32.into())); + run_to_session(6); + assert!(Parachains::is_parathread(32.into())); + assert!(!Parachains::is_parachain(32.into())); + // Deregister it + assert_ok!(Registrar::deregister( Origin::root(), + 32.into(), )); - run_to_block(2); + run_to_session(8); + // It is nothing + assert!(!Parachains::is_parathread(32.into())); + assert!(!Parachains::is_parachain(32.into())); + }); + } - assert_ok!(Registrar::register_parachain( - 2u32.into(), - vec![3; 3].into(), - WASM_MAGIC.to_vec().into(), + #[test] + fn register_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert!(!Parachains::is_parathread(32.into())); + assert_ok!(Registrar::register( + Origin::signed(1), + 32.into(), + test_genesis_head(32), + test_validation_code(32), )); + run_to_session(2); + assert!(Parachains::is_parathread(32.into())); + assert_eq!( + Balances::reserved_balance(&1), + ::ParaDeposit::get() + 64 * ::DataDepositPerByte::get() + ); + }); + } - let orig_bal = Balances::free_balance(&3u64); - - // Register a new parathread - assert_ok!(Registrar::register_parathread( - Origin::signed(3u64), - 8u32.into(), - vec![3; 3].into(), - WASM_MAGIC.to_vec().into(), + #[test] + fn register_handles_basic_errors() { + new_test_ext().execute_with(|| { + // Successfully register 32 + assert_ok!(Registrar::register( + Origin::signed(1), + 32.into(), + test_genesis_head(::MaxHeadSize::get() as usize), + test_validation_code(::MaxCodeSize::get() as usize), )); - // deposit should be taken (reserved) - assert_eq!(Balances::free_balance(3u64) + ParathreadDeposit::get(), orig_bal); - assert_eq!(Balances::reserved_balance(3u64), ParathreadDeposit::get()); - - run_to_block(10); + run_to_session(2); - assert_ok!(Registrar::deregister_parachain(2u32.into())); + assert_ok!(Registrar::deregister(Origin::root(), 32u32.into())); - assert_ok!(Registrar::deregister_parathread( - runtime_parachains::Origin::Parachain(8u32.into()).into() - )); - - // reserved balance should be returned. - assert_eq!(Balances::free_balance(3u64), orig_bal); - assert_eq!(Balances::reserved_balance(3u64), 0); + // Can't do it again + assert_noop!(Registrar::register( + Origin::signed(1), + 32.into(), + test_genesis_head(::MaxHeadSize::get() as usize), + test_validation_code(::MaxCodeSize::get() as usize), + ), Error::::AlreadyRegistered); + + // Head Size Check + assert_noop!(Registrar::register( + Origin::signed(2), + 23.into(), + test_genesis_head((::MaxHeadSize::get() + 1) as usize), + test_validation_code(::MaxCodeSize::get() as usize), + ), Error::::HeadDataTooLarge); + + // Code Size Check + assert_noop!(Registrar::register( + Origin::signed(2), + 23.into(), + test_genesis_head(::MaxHeadSize::get() as usize), + test_validation_code((::MaxCodeSize::get() + 1) as usize), + ), Error::::CodeTooLarge); + + // Needs enough funds for deposit + assert_noop!(Registrar::register( + Origin::signed(1337), + 23.into(), + test_genesis_head(::MaxHeadSize::get() as usize), + test_validation_code(::MaxCodeSize::get() as usize), + ), BalancesError::::InsufficientBalance); }); } #[test] - fn swap_handles_funds_correctly() { + fn deregister_works() { new_test_ext().execute_with(|| { run_to_block(1); - - assert_ok!(Registrar::enable_parathread_registration( + assert!(!Parachains::is_parathread(32.into())); + assert_ok!(Registrar::register( + Origin::signed(1), + 32.into(), + test_genesis_head(32), + test_validation_code(32), + )); + assert_eq!( + Balances::reserved_balance(&1), + ::ParaDeposit::get() + 64 * ::DataDepositPerByte::get() + ); + run_to_session(2); + assert!(Parachains::is_parathread(32.into())); + assert_ok!(Registrar::deregister( Origin::root(), + 32.into(), )); - run_to_block(2); - - let initial_1_balance = Balances::free_balance(1); - let initial_2_balance = Balances::free_balance(2); + run_to_session(4); + assert!(paras::Module::::lifecycle(32.into()).is_none()); + assert_eq!(Balances::reserved_balance(&1), 0); + }); + } - // User 1 register a new parathread - assert_ok!(Registrar::register_parathread( + #[test] + fn deregister_handles_basic_errors() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert!(!Parachains::is_parathread(32.into())); + assert_ok!(Registrar::register( Origin::signed(1), - 8u32.into(), - vec![1; 3].into(), - WASM_MAGIC.to_vec().into(), + 32.into(), + test_genesis_head(32), + test_validation_code(32), )); + run_to_session(2); + assert!(Parachains::is_parathread(32.into())); + // Origin check + assert_noop!(Registrar::deregister( + Origin::signed(1), + 32.into(), + ), BadOrigin); + // not registered + assert_noop!(Registrar::deregister( + Origin::root(), + 33.into(), + ), Error::::NotParathread); + assert_ok!(Registrar::make_parachain(32.into())); + run_to_session(4); + // Cant directly deregister parachain + assert_noop!(Registrar::deregister( + Origin::root(), + 32.into(), + ), Error::::NotParathread); + }); + } - assert_ok!(Registrar::register_parachain( - 2u32.into(), - vec![1; 3].into(), - WASM_MAGIC.to_vec().into(), + #[test] + fn swap_works() { + new_test_ext().execute_with(|| { + // Successfully register 23 and 32 + assert_ok!(Registrar::register( + Origin::signed(1), + 23.into(), + test_genesis_head(::MaxHeadSize::get() as usize), + test_validation_code(::MaxCodeSize::get() as usize), )); + assert_ok!(Registrar::register( + Origin::signed(2), + 32.into(), + test_genesis_head(::MaxHeadSize::get() as usize), + test_validation_code(::MaxCodeSize::get() as usize), + )); + run_to_session(2); + + // Upgrade 23 into a parachain + assert_ok!(Registrar::make_parachain(23.into())); - run_to_block(9); + run_to_session(4); - // Swap the parachain and parathread - assert_ok!(Registrar::swap(runtime_parachains::Origin::Parachain(2u32.into()).into(), 8u32.into())); - assert_ok!(Registrar::swap(runtime_parachains::Origin::Parachain(8u32.into()).into(), 2u32.into())); + // Roles are as we expect + assert!(Parachains::is_parachain(23.into())); + assert!(!Parachains::is_parathread(23.into())); + assert!(!Parachains::is_parachain(32.into())); + assert!(Parachains::is_parathread(32.into())); - run_to_block(15); + // Both paras initiate a swap + assert_ok!(Registrar::swap( + para_origin(23.into()), + 32.into() + )); + assert_ok!(Registrar::swap( + para_origin(32.into()), + 23.into() + )); + + run_to_session(6); // Deregister a parathread that was originally a parachain - assert_ok!(Registrar::deregister_parathread(runtime_parachains::Origin::Parachain(2u32.into()).into())); + assert_eq!(Parachains::lifecycle(23u32.into()), Some(ParaLifecycle::Parathread)); + assert_ok!(Registrar::deregister(runtime_parachains::Origin::Parachain(23u32.into()).into(), 23u32.into())); run_to_block(21); - // Funds are correctly returned - assert_eq!(Balances::free_balance(1), initial_1_balance); - assert_eq!(Balances::free_balance(2), initial_2_balance); + // Roles are swapped + assert!(!Parachains::is_parachain(23.into())); + assert!(Parachains::is_parathread(23.into())); + assert!(Parachains::is_parachain(32.into())); + assert!(!Parachains::is_parathread(32.into())); }); } #[test] fn cannot_register_until_para_is_cleaned_up() { new_test_ext().execute_with(|| { - run_to_block(2); + run_to_block(1); - assert_ok!(Registrar::register_parachain( + assert_ok!(Registrar::register( + Origin::signed(1), 1u32.into(), vec![1; 3].into(), WASM_MAGIC.to_vec().into(), )); // 2 session changes to fully onboard. - run_to_block(12); - - assert_eq!(Parachains::lifecycle(1u32.into()), Some(ParaLifecycle::Parachain)); + run_to_session(2); - assert_ok!(Registrar::deregister_parachain(1u32.into())); - run_to_block(13); + assert_eq!(Parachains::lifecycle(1u32.into()), Some(ParaLifecycle::Parathread)); + assert_ok!(Registrar::deregister(Origin::root(), 1u32.into())); - assert_eq!(Parachains::lifecycle(1u32.into()), Some(ParaLifecycle::OffboardingParachain)); + // Cannot register while it is offboarding. + run_to_session(3); - assert_noop!(Registrar::register_parachain( + assert_noop!(Registrar::register( + Origin::signed(1), 1u32.into(), vec![1; 3].into(), WASM_MAGIC.to_vec().into(), - ), Error::::ParaAlreadyExists); + ), Error::::AlreadyRegistered); - // Need 2 session changes to see the effect, which takes place by block 13. - run_to_block(18); + // By session 4, it is offboarded, and we can register again. + run_to_session(4); - assert!(Parachains::lifecycle(1u32.into()).is_none()); - assert_ok!(Registrar::register_parachain( + assert_ok!(Registrar::register( + Origin::signed(1), 1u32.into(), vec![1; 3].into(), WASM_MAGIC.to_vec().into(), @@ -748,3 +840,114 @@ mod tests { }); } } + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking { + use super::{*, Module as Registrar}; + use frame_system::RawOrigin; + use frame_support::assert_ok; + use sp_runtime::traits::Bounded; + use crate::traits::{Registrar as RegistrarT}; + use runtime_parachains::{paras, shared, Origin as ParaOrigin}; + + use frame_benchmarking::{benchmarks, whitelisted_caller}; + + fn assert_last_event(generic_event: ::Event) { + let events = frame_system::Module::::events(); + let system_event: ::Event = generic_event.into(); + // compare to the last event record + let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); + } + + fn register_para(id: u32) -> ParaId { + let para = ParaId::from(id); + let genesis_head = Registrar::::worst_head_data(); + let validation_code = Registrar::::worst_validation_code(); + let caller: T::AccountId = whitelisted_caller(); + T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + assert_ok!(Registrar::::register(RawOrigin::Signed(caller).into(), para, genesis_head, validation_code)); + return para; + } + + fn para_origin(id: u32) -> ParaOrigin { + ParaOrigin::Parachain(id.into()) + } + + // This function moves forward to the next scheduled session for parachain lifecycle upgrades. + fn next_scheduled_session() { + shared::Module::::set_session_index( + shared::Module::::scheduled_session() + ); + paras::Module::::test_on_new_session(); + } + + benchmarks! { + where_clause { where ParaOrigin: Into<::Origin> } + + register { + let para = ParaId::from(1337); + let genesis_head = Registrar::::worst_head_data(); + let validation_code = Registrar::::worst_validation_code(); + let caller: T::AccountId = whitelisted_caller(); + T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + }: _(RawOrigin::Signed(caller.clone()), para, genesis_head, validation_code) + verify { + assert_last_event::(RawEvent::Registered(para, caller).into()); + assert_eq!(paras::Module::::lifecycle(para), Some(ParaLifecycle::Onboarding)); + next_scheduled_session::(); + assert_eq!(paras::Module::::lifecycle(para), Some(ParaLifecycle::Parathread)); + } + + deregister { + let para = register_para::(1337); + next_scheduled_session::(); + }: _(RawOrigin::Root, para) + verify { + assert_last_event::(RawEvent::Deregistered(para).into()); + } + + swap { + let parathread = register_para::(1337); + let parachain = register_para::(1338); + + let parathread_origin = para_origin(1337); + let parachain_origin = para_origin(1338); + + // Actually finish registration process + next_scheduled_session::(); + + // Upgrade the parachain + Registrar::::make_parachain(parachain)?; + next_scheduled_session::(); + + assert_eq!(paras::Module::::lifecycle(parachain), Some(ParaLifecycle::Parachain)); + assert_eq!(paras::Module::::lifecycle(parathread), Some(ParaLifecycle::Parathread)); + + Registrar::::swap(parathread_origin.into(), parachain)?; + }: { + Registrar::::swap(parachain_origin.into(), parathread)?; + } verify { + next_scheduled_session::(); + // Swapped! + assert_eq!(paras::Module::::lifecycle(parachain), Some(ParaLifecycle::Parathread)); + assert_eq!(paras::Module::::lifecycle(parathread), Some(ParaLifecycle::Parachain)); + } + } + + #[cfg(test)] + mod tests { + use super::*; + use crate::integration_tests::{new_test_ext, Test}; + use frame_support::assert_ok; + + #[test] + fn test_benchmarks() { + new_test_ext().execute_with(|| { + assert_ok!(test_benchmark_register::()); + assert_ok!(test_benchmark_deregister::()); + assert_ok!(test_benchmark_swap::()); + }); + } + } +} diff --git a/runtime/common/src/slots.rs b/runtime/common/src/slots.rs index 88c5400eab79..fdd86ad402cf 100644 --- a/runtime/common/src/slots.rs +++ b/runtime/common/src/slots.rs @@ -14,27 +14,36 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -//! Auctioning system to determine the set of Parachains in operation. This includes logic for the -//! auctioning mechanism, for locking balance as part of the "payment", and to provide the requisite -//! information for commissioning and decommissioning them. - -use sp_std::{prelude::*, mem::swap, convert::TryInto}; -use sp_runtime::traits::{ - CheckedSub, StaticLookup, Zero, One, CheckedConversion, Hash, AccountIdConversion, -}; -use parity_scale_codec::{Encode, Decode, Codec}; +//! Parathread and parachains leasing system. Allows para IDs to be claimed, the code and data to be initialized and +//! parachain slots (i.e. continuous scheduling) to be leased. Also allows for parachains and parathreads to be +//! swapped. +//! +//! This doesn't handle the mechanics of determining which para ID actually ends up with a parachain lease. This +//! must handled by a separately, through the trait interface that this pallet provides or the root dispatchables. + +use sp_std::prelude::*; +use sp_runtime::traits::{CheckedSub, Zero, CheckedConversion}; use frame_support::{ - decl_module, decl_storage, decl_event, decl_error, ensure, dispatch::DispatchResult, - traits::{Currency, ReservableCurrency, WithdrawReasons, ExistenceRequirement, Get, Randomness}, - weights::{DispatchClass, Weight}, -}; -use primitives::v1::{ - Id as ParaId, ValidationCode, HeadData, + decl_module, decl_storage, decl_event, decl_error, dispatch::DispatchResult, + traits::{Currency, ReservableCurrency, Get}, weights::Weight, }; -use frame_system::{ensure_signed, ensure_root}; -use crate::slot_range::{SlotRange, SLOT_RANGE_COUNT}; +use primitives::v1::Id as ParaId; +use frame_system::ensure_root; +use crate::traits::{Leaser, LeaseError, Registrar}; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; +type LeasePeriodOf = ::BlockNumber; + +pub trait WeightInfo { + fn force_lease() -> Weight; + fn manage_lease_period_start(c: u32, t: u32) -> Weight; +} + +pub struct TestWeightInfo; +impl WeightInfo for TestWeightInfo { + fn force_lease() -> Weight { 0 } + fn manage_lease_period_start(_c: u32, _t:u32) -> Weight { 0 } +} /// The module's configuration trait. pub trait Config: frame_system::Config { @@ -45,256 +54,58 @@ pub trait Config: frame_system::Config { type Currency: ReservableCurrency; /// The parachain registrar type. - type Parachains: Registrar; - - /// The number of blocks over which an auction may be retroactively ended. - type EndingPeriod: Get; + type Registrar: Registrar; /// The number of blocks over which a single period lasts. type LeasePeriod: Get; - /// Something that provides randomness in the runtime. - type Randomness: Randomness; -} - -/// Parachain registration API. -pub trait Registrar { - /// Create a new unique parachain identity for later registration. - fn new_id() -> ParaId; - - /// Checks whether the given initial head data size falls within the limit. - fn head_data_size_allowed(head_data_size: u32) -> bool; - - /// Checks whether the given validation code falls within the limit. - fn code_size_allowed(code_size: u32) -> bool; - - /// Register a parachain with given `code` and `initial_head_data`. `id` must not yet be registered or it will - /// result in a error. - /// - /// This does not enforce any code size or initial head data limits, as these - /// are governable and parameters for parachain initialization are often - /// determined long ahead-of-time. Not checking these values ensures that changes to limits - /// do not invalidate in-progress auction winners. - fn register_para( - id: ParaId, - _parachain: bool, - code: ValidationCode, - initial_head_data: HeadData, - ) -> DispatchResult; - - /// Deregister a parachain with given `id`. If `id` is not currently registered, an error is returned. - fn deregister_para(id: ParaId) -> DispatchResult; -} - -/// Auxilliary for when there's an attempt to swap two parachains/parathreads. -pub trait SwapAux { - /// Result describing whether it is possible to swap two parachains. Doesn't mutate state. - fn ensure_can_swap(one: ParaId, other: ParaId) -> Result<(), &'static str>; - - /// Updates any needed state/references to enact a logical swap of two parachains. Identity, - /// code and `head_data` remain equivalent for all parachains/threads, however other properties - /// such as leases, deposits held and thread/chain nature are swapped. - /// - /// May only be called on a state that `ensure_can_swap` has previously returned `Ok` for: if this is - /// not the case, the result is undefined. May only return an error if `ensure_can_swap` also returns - /// an error. - fn on_swap(one: ParaId, other: ParaId) -> Result<(), &'static str>; -} - -/// A sub-bidder identifier. Used to distinguish between different logical bidders coming from the -/// same account ID. -pub type SubId = u32; -/// An auction index. We count auctions in this type. -pub type AuctionIndex = u32; - -/// A bidder identifier, which is just the combination of an account ID and a sub-bidder ID. -/// This is called `NewBidder` in order to distinguish between bidders that would deploy a *new* -/// parachain and pre-existing parachains bidding to renew themselves. -#[derive(Clone, Eq, PartialEq, Encode, Decode)] -#[cfg_attr(feature = "std", derive(Debug))] -pub struct NewBidder { - /// The bidder's account ID; this is the account that funds the bid. - pub who: AccountId, - /// An additional ID to allow the same account ID (and funding source) to have multiple - /// logical bidders. - pub sub: SubId, -} - -/// The desired target of a bidder in an auction. -#[derive(Clone, Eq, PartialEq, Encode, Decode)] -#[cfg_attr(feature = "std", derive(Debug))] -pub enum Bidder { - /// An account ID, funds coming from that account. - New(NewBidder), - - /// An existing parachain, funds coming from the amount locked as part of a previous bid topped - /// up with funds administered by the parachain. - Existing(ParaId), -} - -impl Bidder { - /// Get the account that will fund this bid. - fn funding_account(&self) -> AccountId { - match self { - Bidder::New(new_bidder) => new_bidder.who.clone(), - Bidder::Existing(para_id) => para_id.into_account(), - } - } -} - -/// Information regarding a parachain that will be deployed. -/// -/// We store either the bidder that will be able to set the final deployment information or the -/// information itself. -#[derive(Clone, Eq, PartialEq, Encode, Decode)] -#[cfg_attr(feature = "std", derive(Debug))] -pub enum IncomingParachain { - /// Deploy information not yet set; just the bidder identity. - Unset(NewBidder), - /// Deploy information set only by code hash; so we store the code hash, code size, and head data. - /// - /// The code size must be included so that checks against a maximum code size - /// can be done. If the size of the preimage of the code hash does not match - /// the given code size, it will not be possible to register the parachain. - Fixed { code_hash: Hash, code_size: u32, initial_head_data: HeadData }, - /// Deploy information fully set; so we store the code and head data. - Deploy { code: ValidationCode, initial_head_data: HeadData }, + /// Weight Information for the Extrinsics in the Pallet + type WeightInfo: WeightInfo; } -type LeasePeriodOf = ::BlockNumber; -// Winning data type. This encodes the top bidders of each range together with their bid. -type WinningData = - [Option<(Bidder<::AccountId>, BalanceOf)>; SLOT_RANGE_COUNT]; -// Winners data type. This encodes each of the final winners of a parachain auction, the parachain -// index assigned to them, their winning bid and the range that they won. -type WinnersData = - Vec<(Option::AccountId>>, ParaId, BalanceOf, SlotRange)>; - // This module's storage items. decl_storage! { trait Store for Module as Slots { - /// The number of auctions that have been started so far. - pub AuctionCounter get(fn auction_counter): AuctionIndex; - - /// Ordered list of all `ParaId` values that are managed by this module. This includes - /// chains that are not yet deployed (but have won an auction in the future). - pub ManagedIds get(fn managed_ids): Vec; - - /// Various amounts on deposit for each parachain. An entry in `ManagedIds` implies a non- - /// default entry here. + /// Amounts held on deposit for each (possibly future) leased parachain. /// - /// The actual amount locked on its behalf at any time is the maximum item in this list. The - /// first item in the list is the amount locked for the current Lease Period. Following + /// The actual amount locked on its behalf by any account at any time is the maximum of the second values + /// of the items in this list whose first value is the account. + /// + /// The first item in the list is the amount locked for the current Lease Period. Following /// items are for the subsequent lease periods. /// /// The default value (an empty list) implies that the parachain no longer exists (or never /// existed) as far as this module is concerned. /// /// If a parachain doesn't exist *yet* but is scheduled to exist in the future, then it - /// will be left-padded with one or more zeroes to denote the fact that nothing is held on + /// will be left-padded with one or more `None`s to denote the fact that nothing is held on /// deposit for the non-existent chain currently, but is held at some point in the future. - pub Deposits get(fn deposits): map hasher(twox_64_concat) ParaId => Vec>; - - /// Information relating to the current auction, if there is one. /// - /// The first item in the tuple is the lease period index that the first of the four - /// contiguous lease periods on auction is for. The second is the block number when the - /// auction will "begin to end", i.e. the first block of the Ending Period of the auction. - pub AuctionInfo get(fn auction_info): Option<(LeasePeriodOf, T::BlockNumber)>; - - /// The winning bids for each of the 10 ranges at each block in the final Ending Period of - /// the current auction. The map's key is the 0-based index into the Ending Period. The - /// first block of the ending period is 0; the last is `EndingPeriod - 1`. - pub Winning get(fn winning): map hasher(twox_64_concat) T::BlockNumber => Option>; - - /// Amounts currently reserved in the accounts of the bidders currently winning - /// (sub-)ranges. - pub ReservedAmounts get(fn reserved_amounts): - map hasher(twox_64_concat) Bidder => Option>; - - /// The set of Para IDs that have won and need to be on-boarded at an upcoming lease-period. - /// This is cleared out on the first block of the lease period. - pub OnboardQueue get(fn onboard_queue): map hasher(twox_64_concat) LeasePeriodOf => Vec; - - /// The actual on-boarding information. Only exists when one of the following is true: - /// - It is before the lease period that the parachain should be on-boarded. - /// - The full on-boarding information has not yet been provided and the parachain is not - /// yet due to be off-boarded. - pub Onboarding get(fn onboarding): - map hasher(twox_64_concat) ParaId => - Option<(LeasePeriodOf, IncomingParachain)>; - - /// Off-boarding account; currency held on deposit for the parachain gets placed here if the - /// parachain gets off-boarded; i.e. its lease period is up and it isn't renewed. - pub Offboarding get(fn offboarding): map hasher(twox_64_concat) ParaId => T::AccountId; - } -} - -/// Swap the existence of two items, provided by value, within an ordered list. -/// -/// If neither item exists, or if both items exist this will do nothing. If exactly one of the -/// items exists, then it will be removed and the other inserted. -fn swap_ordered_existence(ids: &mut [T], one: T, other: T) { - let maybe_one_pos = ids.binary_search(&one); - let maybe_other_pos = ids.binary_search(&other); - match (maybe_one_pos, maybe_other_pos) { - (Ok(one_pos), Err(_)) => ids[one_pos] = other, - (Err(_), Ok(other_pos)) => ids[other_pos] = one, - _ => return, - }; - ids.sort(); -} - -impl SwapAux for Module { - fn ensure_can_swap(one: ParaId, other: ParaId) -> Result<(), &'static str> { - if >::contains_key(one) || >::contains_key(other) { - Err("can't swap an undeployed parachain")? - } - Ok(()) - } - fn on_swap(one: ParaId, other: ParaId) -> Result<(), &'static str> { - >::swap(one, other); - >::swap(one, other); - ManagedIds::mutate(|ids| swap_ordered_existence(ids, one, other)); - Ok(()) + /// It is illegal for a `None` value to trail in the list. + pub Leases get(fn lease): map hasher(twox_64_concat) ParaId => Vec)>>; } } decl_event!( pub enum Event where AccountId = ::AccountId, - BlockNumber = ::BlockNumber, LeasePeriod = LeasePeriodOf, ParaId = ParaId, Balance = BalanceOf, { /// A new [lease_period] is beginning. NewLeasePeriod(LeasePeriod), - /// An auction started. Provides its index and the block number where it will begin to - /// close and the first lease period of the quadruplet that is auctioned. - /// [auction_index, lease_period, ending] - AuctionStarted(AuctionIndex, LeasePeriod, BlockNumber), - /// An auction ended. All funds become unreserved. [auction_index] - AuctionClosed(AuctionIndex), - /// Someone won the right to deploy a parachain. Balance amount is deducted for deposit. - /// [bidder, range, parachain_id, amount] - WonDeploy(NewBidder, SlotRange, ParaId, Balance), /// An existing parachain won the right to continue. /// First balance is the extra amount reseved. Second is the total amount reserved. - /// [parachain_id, range, extra_reseved, total_amount] - WonRenewal(ParaId, SlotRange, Balance, Balance), - /// Funds were reserved for a winning bid. First balance is the extra amount reserved. - /// Second is the total. [bidder, extra_reserved, total_amount] - Reserved(AccountId, Balance, Balance), - /// Funds were unreserved since bidder is no longer active. [bidder, amount] - Unreserved(AccountId, Balance), + /// \[parachain_id, leaser, period_begin, period_count, extra_reseved, total_amount\] + Leased(ParaId, AccountId, LeasePeriod, LeasePeriod, Balance, Balance), + /// A para ID value has been claimed. + Claimed(ParaId), } ); decl_error! { pub enum Error for Module { - /// This auction is already in progress. - AuctionInProgress, /// The lease period is in the past. LeasePeriodInPast, /// The origin for this call must be a parachain. @@ -311,14 +122,14 @@ decl_error! { UnsetDeployData, /// The bid must overlap all intersecting ranges. NonIntersectingRange, - /// Not a current auction. - NotCurrentAuction, - /// Not an auction. - NotAuction, /// Given code size is too large. CodeTooLarge, /// Given initial head data is too large. HeadDataTooLarge, + /// The Id given is already in use. + InUse, + /// There was an error with the lease. + LeaseError, } } @@ -326,631 +137,242 @@ decl_module! { pub struct Module for enum Call where origin: T::Origin { type Error = Error; + const LeasePeriod: T::BlockNumber = T::LeasePeriod::get(); + fn deposit_event() = default; fn on_initialize(n: T::BlockNumber) -> Weight { + // If we're beginning a new lease period then handle that. let lease_period = T::LeasePeriod::get(); - let lease_period_index: LeasePeriodOf = (n / lease_period).into(); - - // Check to see if an auction just ended. - if let Some((winning_ranges, auction_lease_period_index)) = Self::check_auction_end(n) { - // Auction is ended now. We have the winning ranges and the lease period index which - // acts as the offset. Handle it. - Self::manage_auction_end( - lease_period_index, - auction_lease_period_index, - winning_ranges, - ); - } - // If we're beginning a new lease period then handle that, too. if (n % lease_period).is_zero() { - Self::manage_lease_period_start(lease_period_index); - } - - 0 - } - - fn on_finalize(now: T::BlockNumber) { - // If the current auction is in it ending period, then ensure that the (sub-)range - // winner information is duplicated from the previous block in case no bids happened - // in this block. - if let Some(offset) = Self::is_ending(now) { - if !>::contains_key(&offset) { - >::insert(offset, - offset.checked_sub(&One::one()) - .and_then(>::get) - .unwrap_or_default() - ); - } + let lease_period_index = n / lease_period; + Self::manage_lease_period_start(lease_period_index) + } else { + 0 } } - /// Create a new auction. - /// - /// This can only happen when there isn't already an auction in progress and may only be - /// called by the root origin. Accepts the `duration` of this auction and the - /// `lease_period_index` of the initial lease period of the four that are to be auctioned. - #[weight = (100_000_000, DispatchClass::Operational)] - pub fn new_auction(origin, - #[compact] duration: T::BlockNumber, - #[compact] lease_period_index: LeasePeriodOf - ) { + /// Just a hotwire into the `lease_out` call, in case Root wants to force some lease to happen + /// independently of any other on-chain mechanism to use it. + #[weight = T::WeightInfo::force_lease()] + fn force_lease(origin, + para: ParaId, + leaser: T::AccountId, + amount: BalanceOf, + period_begin: LeasePeriodOf, + period_count: LeasePeriodOf, + ) -> DispatchResult { ensure_root(origin)?; - ensure!(!Self::is_in_progress(), Error::::AuctionInProgress); - ensure!(lease_period_index >= Self::lease_period_index(), Error::::LeasePeriodInPast); - - // Bump the counter. - let n = ::mutate(|n| { *n += 1; *n }); - - // Set the information. - let ending = >::block_number() + duration; - >::put((lease_period_index, ending)); - - Self::deposit_event(RawEvent::AuctionStarted(n, lease_period_index, ending)) - } - - /// Make a new bid from an account (including a parachain account) for deploying a new - /// parachain. - /// - /// Multiple simultaneous bids from the same bidder are allowed only as long as all active - /// bids overlap each other (i.e. are mutually exclusive). Bids cannot be redacted. - /// - /// - `sub` is the sub-bidder ID, allowing for multiple competing bids to be made by (and - /// funded by) the same account. - /// - `auction_index` is the index of the auction to bid on. Should just be the present - /// value of `AuctionCounter`. - /// - `first_slot` is the first lease period index of the range to bid on. This is the - /// absolute lease period index value, not an auction-specific offset. - /// - `last_slot` is the last lease period index of the range to bid on. This is the - /// absolute lease period index value, not an auction-specific offset. - /// - `amount` is the amount to bid to be held as deposit for the parachain should the - /// bid win. This amount is held throughout the range. - #[weight = 500_000_000] - pub fn bid(origin, - #[compact] sub: SubId, - #[compact] auction_index: AuctionIndex, - #[compact] first_slot: LeasePeriodOf, - #[compact] last_slot: LeasePeriodOf, - #[compact] amount: BalanceOf - ) { - let who = ensure_signed(origin)?; - let bidder = Bidder::New(NewBidder{who: who.clone(), sub}); - Self::handle_bid(bidder, auction_index, first_slot, last_slot, amount)?; + Self::lease_out(para, &leaser, amount, period_begin, period_count) + .map_err(|_| Error::::LeaseError)?; + Ok(()) } + } +} - /// Make a new bid from a parachain account for renewing that (pre-existing) parachain. - /// - /// The origin *must* be a parachain account. - /// - /// Multiple simultaneous bids from the same bidder are allowed only as long as all active - /// bids overlap each other (i.e. are mutually exclusive). Bids cannot be redacted. - /// - /// - `auction_index` is the index of the auction to bid on. Should just be the present - /// value of `AuctionCounter`. - /// - `first_slot` is the first lease period index of the range to bid on. This is the - /// absolute lease period index value, not an auction-specific offset. - /// - `last_slot` is the last lease period index of the range to bid on. This is the - /// absolute lease period index value, not an auction-specific offset. - /// - `amount` is the amount to bid to be held as deposit for the parachain should the - /// bid win. This amount is held throughout the range. - #[weight = 500_000_000] - fn bid_renew(origin, - #[compact] auction_index: AuctionIndex, - #[compact] first_slot: LeasePeriodOf, - #[compact] last_slot: LeasePeriodOf, - #[compact] amount: BalanceOf - ) { - let who = ensure_signed(origin)?; - let para_id = ::try_from_account(&who) - .ok_or(Error::::NotParaOrigin)?; - let bidder = Bidder::Existing(para_id); - Self::handle_bid(bidder, auction_index, first_slot, last_slot, amount)?; - } +impl Module { + /// A new lease period is beginning. We're at the start of the first block of it. + /// + /// We need to on-board and off-board parachains as needed. We should also handle reducing/ + /// returning deposits. + fn manage_lease_period_start(lease_period_index: LeasePeriodOf) -> Weight { + Self::deposit_event(RawEvent::NewLeasePeriod(lease_period_index)); - /// Set the off-boarding information for a parachain. - /// - /// The origin *must* be a parachain account. - /// - /// - `dest` is the destination account to receive the parachain's deposit. - #[weight = 1_000_000_000] - pub fn set_offboarding(origin, dest: ::Source) { - let who = ensure_signed(origin)?; - let dest = T::Lookup::lookup(dest)?; - let para_id = ::try_from_account(&who) - .ok_or(Error::::NotParaOrigin)?; - >::insert(para_id, dest); - } + let old_parachains = T::Registrar::parachains(); + + // Figure out what chains need bringing on. + let mut parachains = Vec::new(); + for (para, mut lease_periods) in Leases::::iter() { + if lease_periods.is_empty() { continue } + // ^^ should never be empty since we would have deleted the entry otherwise. + + if lease_periods.len() == 1 { + // Just one entry, which corresponds to the now-ended lease period. + // + // `para` is now just a parathread. + // + // Unreserve whatever is left. + if let Some((who, value)) = &lease_periods[0] { + T::Currency::unreserve(&who, *value); + } - /// Set the deploy information for a successful bid to deploy a new parachain. - /// - /// - `origin` must be the successful bidder account. - /// - `sub` is the sub-bidder ID of the bidder. - /// - `para_id` is the parachain ID allotted to the winning bidder. - /// - `code_hash` is the hash of the parachain's Wasm validation function. - /// - `initial_head_data` is the parachain's initial head data. - #[weight = 500_000_000] - pub fn fix_deploy_data(origin, - #[compact] sub: SubId, - #[compact] para_id: ParaId, - code_hash: T::Hash, - code_size: u32, - initial_head_data: HeadData, - ) { - let who = ensure_signed(origin)?; - let (starts, details) = >::get(¶_id) - .ok_or(Error::::ParaNotOnboarding)?; - if let IncomingParachain::Unset(ref nb) = details { - ensure!(nb.who == who && nb.sub == sub, Error::::InvalidOrigin); + // Remove the now-empty lease list. + Leases::::remove(para); } else { - Err(Error::::AlreadyRegistered)? - } + // The parachain entry has leased future periods. - ensure!( - T::Parachains::head_data_size_allowed(initial_head_data.0.len() as _), - Error::::HeadDataTooLarge, - ); - ensure!( - T::Parachains::code_size_allowed(code_size), - Error::::CodeTooLarge, - ); - - let item = (starts, IncomingParachain::Fixed{code_hash, code_size, initial_head_data}); - >::insert(¶_id, item); - } - - /// Note a new parachain's code. - /// - /// This must be called after `fix_deploy_data` and `code` must be the preimage of the - /// `code_hash` passed there for the same `para_id`. - /// - /// This may be called before or after the beginning of the parachain's first lease period. - /// If called before then the parachain will become active at the first block of its - /// starting lease period. If after, then it will become active immediately after this call. - /// - /// - `_origin` is irrelevant. - /// - `para_id` is the parachain ID whose code will be elaborated. - /// - `code` is the preimage of the registered `code_hash` of `para_id`. - #[weight = 5_000_000_000] - pub fn elaborate_deploy_data( - _origin, - #[compact] para_id: ParaId, - code: ValidationCode, - ) -> DispatchResult { - let (starts, details) = >::get(¶_id) - .ok_or(Error::::ParaNotOnboarding)?; - if let IncomingParachain::Fixed{code_hash, code_size, initial_head_data} = details { - ensure!(code.0.len() as u32 == code_size, Error::::InvalidCode); - ensure!(::Hashing::hash(&code.0) == code_hash, Error::::InvalidCode); - - if starts > Self::lease_period_index() { - // Hasn't yet begun. Replace the on-boarding entry with the new information. - let item = (starts, IncomingParachain::Deploy{code, initial_head_data}); - >::insert(¶_id, item); - } else { - // Should have already begun. Remove the on-boarding entry and register the - // parachain for its immediate start. - >::remove(¶_id); - let _ = T::Parachains:: - register_para(para_id, true, code, initial_head_data); - } + // We need to pop the first deposit entry, which corresponds to the now- + // ended lease period. + let maybe_ended_lease = lease_periods.remove(0); - Ok(()) - } else { - Err(Error::::UnsetDeployData)? - } - } - } -} + Leases::::insert(para, &lease_periods); -impl Module { - /// Deposit currently held for a particular parachain that we administer. - fn deposit_held(para_id: &ParaId) -> BalanceOf { - >::get(para_id).into_iter().max().unwrap_or_else(Zero::zero) - } + // If we *were* active in the last period and so have ended a lease... + if let Some(ended_lease) = maybe_ended_lease { + // Then we need to get the new amount that should continue to be held on + // deposit for the parachain. + let now_held = Self::deposit_held(para, &ended_lease.0); - /// True if an auction is in progress. - pub fn is_in_progress() -> bool { - >::exists() - } + // If this is less than what we were holding for this leaser's now-ended lease, then + // unreserve it. + if let Some(rebate) = ended_lease.1.checked_sub(&now_held) { + T::Currency::unreserve( &ended_lease.0, rebate); + } + } - /// Returns `Some(n)` if the now block is part of the ending period of an auction, where `n` - /// represents how far into the ending period this block is. Otherwise, returns `None`. - pub fn is_ending(now: T::BlockNumber) -> Option { - if let Some((_, early_end)) = >::get() { - if let Some(after_early_end) = now.checked_sub(&early_end) { - if after_early_end < T::EndingPeriod::get() { - return Some(after_early_end) + // If we have an active lease in the new period, then add to the current parachains + if lease_periods[0].is_some() { + parachains.push(para); } } } - None - } + parachains.sort(); - /// Returns the current lease period. - fn lease_period_index() -> LeasePeriodOf { - (>::block_number() / T::LeasePeriod::get()).into() - } - - /// Some when the auction's end is known (with the end block number). None if it is unknown. - /// If `Some` then the block number must be at most the previous block and at least the - /// previous block minus `T::EndingPeriod::get()`. - /// - /// This mutates the state, cleaning up `AuctionInfo` and `Winning` in the case of an auction - /// ending. An immediately subsequent call with the same argument will always return `None`. - fn check_auction_end(now: T::BlockNumber) -> Option<(WinningData, LeasePeriodOf)> { - if let Some((lease_period_index, early_end)) = >::get() { - let ending_period = T::EndingPeriod::get(); - if early_end + ending_period == now { - // Just ended! - let (seed, _) = T::Randomness::random_seed(); - let offset = T::BlockNumber::decode(&mut seed.as_ref()) - .expect("secure hashes always bigger than block numbers; qed") % ending_period; - let res = >::get(offset).unwrap_or_default(); - let mut i = T::BlockNumber::zero(); - while i < ending_period { - >::remove(i); - i += One::one(); - } - >::kill(); - return Some((res, lease_period_index)) + for para in parachains.iter() { + if old_parachains.binary_search(para).is_err() { + // incoming. + let res = T::Registrar::make_parachain(*para); + debug_assert!(res.is_ok()); } } - None - } - /// Auction just ended. We have the current lease period, the auction's lease period (which - /// is guaranteed to be at least the current period) and the bidders that were winning each - /// range at the time of the auction's close. - fn manage_auction_end( - lease_period_index: LeasePeriodOf, - auction_lease_period_index: LeasePeriodOf, - winning_ranges: WinningData, - ) { - // First, unreserve all amounts that were reserved for the bids. We will later deduct the - // amounts from the bidders that ended up being assigned the slot so there's no need to - // special-case them here. - for (bidder, _) in winning_ranges.iter().filter_map(|x| x.as_ref()) { - if let Some(amount) = >::take(bidder) { - T::Currency::unreserve(&bidder.funding_account(), amount); + for para in old_parachains.iter() { + if parachains.binary_search(para).is_err() { + // outgoing. + let res = T::Registrar::make_parathread(*para); + debug_assert!(res.is_ok()); } } - // Next, calculate the winning combination of slots and thus the final winners of the - // auction. - let winners = Self::calculate_winners(winning_ranges, T::Parachains::new_id); - - Self::deposit_event(RawEvent::AuctionClosed(Self::auction_counter())); - - // Go through those winners and deduct their bid, updating our table of deposits - // accordingly. - for (maybe_new_deploy, para_id, amount, range) in winners.into_iter() { - match maybe_new_deploy { - Some(bidder) => { - // For new deployments we ensure the full amount is deducted. This should always - // succeed as we just unreserved the same amount above. - if T::Currency::withdraw( - &bidder.who, - amount, - WithdrawReasons::FEE, - ExistenceRequirement::AllowDeath - ).is_err() { - continue; - } - - // Add para IDs of any chains that will be newly deployed to our set of managed - // IDs. - ManagedIds::mutate(|ids| - if let Err(pos) = ids.binary_search(¶_id) { - ids.insert(pos, para_id) - } else { - // This can't happen as it's a winner being newly - // deployed and thus the para_id shouldn't already be being managed. - } - ); - Self::deposit_event(RawEvent::WonDeploy(bidder.clone(), range, para_id, amount)); - - // Add a deployment record so we know to on-board them at the appropriate - // juncture. - let begin_offset = >::from(range.as_pair().0 as u32); - let begin_lease_period = auction_lease_period_index + begin_offset; - >::mutate(begin_lease_period, |starts| starts.push(para_id)); - // Add a default off-boarding account which matches the original bidder - >::insert(¶_id, &bidder.who); - let entry = (begin_lease_period, IncomingParachain::Unset(bidder)); - >::insert(¶_id, entry); - } - None => { - // For renewals, reserve any extra on top of what we already have held - // on deposit for their chain. - let extra = if let Some(additional) = - amount.checked_sub(&Self::deposit_held(¶_id)) - { - if T::Currency::withdraw( - ¶_id.into_account(), - additional, - WithdrawReasons::FEE, - ExistenceRequirement::AllowDeath - ).is_err() { - continue; - } - additional - } else { - Default::default() - }; - Self::deposit_event(RawEvent::WonRenewal(para_id, range, extra, amount)); - } - } + T::WeightInfo::manage_lease_period_start( + old_parachains.len() as u32, + parachains.len() as u32, + ) + } +} - // Finally, we update the deposit held so it is `amount` for the new lease period - // indices that were won in the auction. - let maybe_offset = auction_lease_period_index - .checked_sub(&lease_period_index) - .and_then(|x| x.checked_into::()); - if let Some(offset) = maybe_offset { - // Should always succeed; for it to fail it would mean we auctioned a lease period - // that already ended. - - // The lease period index range (begin, end) that newly belongs to this parachain - // ID. We need to ensure that it features in `Deposits` to prevent it from being - // reaped too early (any managed parachain whose `Deposits` set runs low will be - // removed). - let pair = range.as_pair(); - let pair = (pair.0 as usize + offset, pair.1 as usize + offset); - >::mutate(para_id, |d| { - // Left-pad with zeroes as necessary. - if d.len() < pair.0 { - d.resize_with(pair.0, Default::default); - } - // Then place the deposit values for as long as the chain should exist. - for i in pair.0 ..= pair.1 { - if d.len() > i { - // The chain bought the same lease period twice. Just take the maximum. - d[i] = d[i].max(amount); - } else if d.len() == i { - d.push(amount); - } else { - unreachable!("earlier resize means it must be >= i; qed") - } - } - }); - } - } +impl crate::traits::OnSwap for Module { + fn on_swap(one: ParaId, other: ParaId) { + Leases::::mutate(one, |x| + Leases::::mutate(other, |y| + sp_std::mem::swap(x, y) + ) + ) } +} - /// A new lease period is beginning. We're at the start of the first block of it. - /// - /// We need to on-board and off-board parachains as needed. We should also handle reducing/ - /// returning deposits. - fn manage_lease_period_start(lease_period_index: LeasePeriodOf) { - Self::deposit_event(RawEvent::NewLeasePeriod(lease_period_index)); - // First, bump off old deposits and decommission any managed chains that are coming - // to a close. - ManagedIds::mutate(|ids| { - let new = ids.drain(..).filter(|id| { - let mut d = >::get(id); - if !d.is_empty() { - // ^^ should always be true, since we would have deleted the entry otherwise. - - if d.len() == 1 { - // Just one entry, which corresponds to the now-ended lease period. Time - // to decommission this chain. - if >::take(id).is_none() { - // Only unregister it if it was actually registered in the first place. - // If the on-boarding entry still existed, then it was never actually - // commissioned. - let _ = T::Parachains::deregister_para(id.clone()); - } - // Return the full deposit to the off-boarding account. - T::Currency::deposit_creating(&>::take(id), d[0]); - // Remove the now-empty deposits set and don't keep the ID around. - >::remove(id); - false +impl Leaser for Module { + type AccountId = T::AccountId; + type LeasePeriod = T::BlockNumber; + type Currency = T::Currency; + + fn lease_out( + para: ParaId, + leaser: &Self::AccountId, + amount: >::Balance, + period_begin: Self::LeasePeriod, + period_count: Self::LeasePeriod, + ) -> Result<(), LeaseError> { + // Finally, we update the deposit held so it is `amount` for the new lease period + // indices that were won in the auction. + let offset = period_begin + .checked_sub(&Self::lease_period_index()) + .and_then(|x| x.checked_into::()) + .ok_or(LeaseError::AlreadyEnded)?; + + // offset is the amount into the `Deposits` items list that our lease begins. `period_count` + // is the number of items that it lasts for. + + // The lease period index range (begin, end) that newly belongs to this parachain + // ID. We need to ensure that it features in `Deposits` to prevent it from being + // reaped too early (any managed parachain whose `Deposits` set runs low will be + // removed). + Leases::::try_mutate(para, |d| { + // Left-pad with `None`s as necessary. + if d.len() < offset { + d.resize_with(offset, || { None }); + } + let period_count_usize = period_count.checked_into::() + .ok_or(LeaseError::AlreadyEnded)?; + // Then place the deposit values for as long as the chain should exist. + for i in offset .. (offset + period_count_usize) { + if d.len() > i { + // Already exists but it's `None`. That means a later slot was already leased. + // No problem. + if d[i] == None { + d[i] = Some((leaser.clone(), amount)); } else { - // The parachain entry is continuing into the next lease period. - // We need to pop the first deposit entry, which corresponds to the now- - // ended lease period. - let outgoing = d[0]; - d.remove(0); - >::insert(id, &d); - // Then we need to get the new amount that should continue to be held on - // deposit for the parachain. - let new_held = d.into_iter().max().unwrap_or_default(); - // If this is less than what we were holding previously, then return it - // to the parachain itself. - if let Some(rebate) = outgoing.checked_sub(&new_held) { - T::Currency::deposit_creating( - &id.into_account(), - rebate - ); - } - // We keep this entry around until the next lease period. - true + // The chain tried to lease the same period twice. This might be a griefing + // attempt. + // + // We bail, not giving any lease and leave it for governance to sort out. + return Err(LeaseError::AlreadyLeased); } + } else if d.len() == i { + // Doesn't exist. This is usual. + d.push(Some((leaser.clone(), amount))); } else { - false + // earlier resize means it must be >= i; qed + // defensive code though since we really don't want to panic here. } - }).collect::>(); - *ids = new; - }); + } - // Deploy any new chains that are due to be commissioned. - for para_id in >::take(lease_period_index) { - if let Some((_, IncomingParachain::Deploy{code, initial_head_data})) - = >::get(¶_id) - { - // The chain's deployment data is set; go ahead and register it, and remove the - // now-redundant on-boarding entry. - let _ = T::Parachains:: - register_para(para_id.clone(), true, code, initial_head_data); - // ^^ not much we can do if it fails for some reason. - >::remove(para_id) + // Figure out whether we already have some funds of `leaser` held in reserve for `para_id`. + // If so, then we can deduct those from the amount that we need to reserve. + let maybe_additional = amount.checked_sub(&Self::deposit_held(para, &leaser)); + if let Some(ref additional) = maybe_additional { + T::Currency::reserve(&leaser, *additional) + .map_err(|_| LeaseError::ReserveFailed)?; } - } - } - /// Actually place a bid in the current auction. - /// - /// - `bidder`: The account that will be funding this bid. - /// - `auction_index`: The auction index of the bid. For this to succeed, must equal - /// the current value of `AuctionCounter`. - /// - `first_slot`: The first lease period index of the range to be bid on. - /// - `last_slot`: The last lease period index of the range to be bid on (inclusive). - /// - `amount`: The total amount to be the bid for deposit over the range. - pub fn handle_bid( - bidder: Bidder, - auction_index: u32, - first_slot: LeasePeriodOf, - last_slot: LeasePeriodOf, - amount: BalanceOf - ) -> DispatchResult { - // Bidding on latest auction. - ensure!(auction_index == ::get(), Error::::NotCurrentAuction); - // Assume it's actually an auction (this should never fail because of above). - let (first_lease_period, _) = >::get().ok_or(Error::::NotAuction)?; - - // Our range. - let range = SlotRange::new_bounded(first_lease_period, first_slot, last_slot)?; - // Range as an array index. - let range_index = range as u8 as usize; - // The offset into the auction ending set. - let offset = Self::is_ending(>::block_number()).unwrap_or_default(); - // The current winning ranges. - let mut current_winning = >::get(offset) - .or_else(|| offset.checked_sub(&One::one()).and_then(>::get)) - .unwrap_or_default(); - // If this bid beat the previous winner of our range. - if current_winning[range_index].as_ref().map_or(true, |last| amount > last.1) { - // This must overlap with all existing ranges that we're winning on or it's invalid. - ensure!(current_winning.iter() - .enumerate() - .all(|(i, x)| x.as_ref().map_or(true, |(w, _)| - w != &bidder || range.intersects(i.try_into() - .expect("array has SLOT_RANGE_COUNT items; index never reaches that value; qed") - ) - )), - Error::::NonIntersectingRange, + let reserved = maybe_additional.unwrap_or_default(); + Self::deposit_event( + RawEvent::Leased(para, leaser.clone(), period_begin, period_count, reserved, amount) ); - // Ok; we are the new winner of this range - reserve the additional amount and record. - - // Get the amount already held on deposit on our behalf if this is a renewal bid from - // an existing parachain. - let deposit_held = if let Bidder::Existing(ref bidder_para_id) = bidder { - Self::deposit_held(bidder_para_id) - } else { - Zero::zero() - }; - // Get the amount already reserved in any prior and still active bids by us. - let already_reserved = - >::get(&bidder).unwrap_or_default() + deposit_held; - // If these don't already cover the bid... - if let Some(additional) = amount.checked_sub(&already_reserved) { - // ...then reserve some more funds from their account, failing if there's not - // enough funds. - T::Currency::reserve(&bidder.funding_account(), additional)?; - // ...and record the amount reserved. - >::insert(&bidder, amount); - - Self::deposit_event(RawEvent::Reserved( - bidder.funding_account(), - additional, - amount - )); - } + Ok(()) + }) + } - // Return any funds reserved for the previous winner if they no longer have any active - // bids. - let mut outgoing_winner = Some((bidder, amount)); - swap(&mut current_winning[range_index], &mut outgoing_winner); - if let Some((who, _)) = outgoing_winner { - if current_winning.iter() - .filter_map(Option::as_ref) - .all(|&(ref other, _)| other != &who) - { - // Previous bidder is no longer winning any ranges: unreserve their funds. - if let Some(amount) = >::take(&who) { - // It really should be reserved; there's not much we can do here on fail. - let _ = T::Currency::unreserve(&who.funding_account(), amount); - - Self::deposit_event(RawEvent::Unreserved(who.funding_account(), amount)); - } + fn deposit_held(para: ParaId, leaser: &Self::AccountId) -> >::Balance { + Leases::::get(para) + .into_iter() + .map(|lease| { + match lease { + Some((who, amount)) => { + if &who == leaser { amount } else { Zero::zero() } + }, + None => Zero::zero(), } - } - // Update the range winner. - >::insert(offset, ¤t_winning); - } - Ok(()) + }) + .max() + .unwrap_or_else(Zero::zero) } - /// Calculate the final winners from the winning slots. - /// - /// This is a simple dynamic programming algorithm designed by Al, the original code is at: - /// https://github.com/w3f/consensus/blob/master/NPoS/auctiondynamicthing.py - fn calculate_winners( - mut winning: WinningData, - new_id: impl Fn() -> ParaId - ) -> WinnersData { - let winning_ranges = { - let mut best_winners_ending_at: - [(Vec, BalanceOf); 4] = Default::default(); - let best_bid = |range: SlotRange| { - winning[range as u8 as usize].as_ref() - .map(|(_, amount)| *amount * (range.len() as u32).into()) - }; - for i in 0..4 { - let r = SlotRange::new_bounded(0, 0, i as u32).expect("`i < 4`; qed"); - if let Some(bid) = best_bid(r) { - best_winners_ending_at[i] = (vec![r], bid); - } - for j in 0..i { - let r = SlotRange::new_bounded(0, j as u32 + 1, i as u32) - .expect("`i < 4`; `j < i`; `j + 1 < 4`; qed"); - if let Some(mut bid) = best_bid(r) { - bid += best_winners_ending_at[j].1; - if bid > best_winners_ending_at[i].1 { - let mut new_winners = best_winners_ending_at[j].0.clone(); - new_winners.push(r); - best_winners_ending_at[i] = (new_winners, bid); - } - } else { - if best_winners_ending_at[j].1 > best_winners_ending_at[i].1 { - best_winners_ending_at[i] = best_winners_ending_at[j].clone(); - } - } - } - } - let [_, _, _, (winning_ranges, _)] = best_winners_ending_at; - winning_ranges - }; - - winning_ranges.into_iter().map(|r| { - let mut final_winner = (Bidder::Existing(Default::default()), Default::default()); - swap(&mut final_winner, winning[r as u8 as usize].as_mut() - .expect("none values are filtered out in previous logic; qed")); - let (slot_winner, bid) = final_winner; - match slot_winner { - Bidder::New(new_bidder) => (Some(new_bidder), new_id(), bid, r), - Bidder::Existing(para_id) => (None, para_id, bid, r), - } - }).collect::>() + fn lease_period() -> Self::LeasePeriod { + T::LeasePeriod::get() + } + + fn lease_period_index() -> Self::LeasePeriod { + >::block_number() / T::LeasePeriod::get() } } + /// tests for this module #[cfg(test)] mod tests { use super::*; - use std::{collections::HashMap, cell::RefCell}; use sp_core::H256; - use sp_runtime::traits::{BlakeTwo256, Hash, IdentityLookup}; + use sp_runtime::traits::{BlakeTwo256, IdentityLookup}; use frame_support::{ - parameter_types, assert_ok, assert_noop, + parameter_types, assert_ok, traits::{OnInitialize, OnFinalize} }; use pallet_balances; - use primitives::v1::{BlockNumber, Header, Id as ParaId}; - use crate::slots; + use primitives::v1::{BlockNumber, Header}; + use crate::{slots, mock::TestRegistrar}; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -975,7 +397,6 @@ mod tests { type BaseCallFilter = (); type BlockWeights = (); type BlockLength = (); - type DbWeight = (); type Origin = Origin; type Call = Call; type Index = u64; @@ -987,6 +408,7 @@ mod tests { type Header = Header; type Event = Event; type BlockHashCount = BlockHashCount; + type DbWeight = (); type Version = (); type PalletInfo = PalletInfo; type AccountData = pallet_balances::AccountData; @@ -1006,88 +428,28 @@ mod tests { type DustRemoval = (); type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; - type MaxLocks = (); type WeightInfo = (); + type MaxLocks = (); } - thread_local! { - pub static PARACHAIN_COUNT: RefCell = RefCell::new(0); - pub static PARACHAINS: - RefCell> = RefCell::new(HashMap::new()); - } - - const MAX_CODE_SIZE: u32 = 100; - const MAX_HEAD_DATA_SIZE: u32 = 10; - - pub struct TestParachains; - impl Registrar for TestParachains { - fn new_id() -> ParaId { - PARACHAIN_COUNT.with(|p| { - *p.borrow_mut() += 1; - (*p.borrow() - 1).into() - }) - } - - fn head_data_size_allowed(head_data_size: u32) -> bool { - head_data_size <= MAX_HEAD_DATA_SIZE - } - - fn code_size_allowed(code_size: u32) -> bool { - code_size <= MAX_CODE_SIZE - } - - fn register_para( - id: ParaId, - _parachain: bool, - code: ValidationCode, - initial_head_data: HeadData, - ) -> DispatchResult { - PARACHAINS.with(|p| { - if p.borrow().contains_key(&id.into()) { - panic!("ID already exists") - } - p.borrow_mut().insert(id.into(), (code, initial_head_data)); - Ok(()) - }) - } - fn deregister_para(id: ParaId) -> DispatchResult { - PARACHAINS.with(|p| { - if !p.borrow().contains_key(&id.into()) { - panic!("ID doesn't exist") - } - p.borrow_mut().remove(&id.into()); - Ok(()) - }) - } - } - - fn reset_count() { - PARACHAIN_COUNT.with(|p| *p.borrow_mut() = 0); - } - - fn with_parachains(f: impl FnOnce(&HashMap) -> T) -> T { - PARACHAINS.with(|p| f(&*p.borrow())) - } - - parameter_types!{ + parameter_types! { pub const LeasePeriod: BlockNumber = 10; - pub const EndingPeriod: BlockNumber = 3; + pub const ParaDeposit: u64 = 1; } impl Config for Test { type Event = Event; type Currency = Balances; - type Parachains = TestParachains; + type Registrar = TestRegistrar; type LeasePeriod = LeasePeriod; - type EndingPeriod = EndingPeriod; - type Randomness = RandomnessCollectiveFlip; + type WeightInfo = crate::slots::TestWeightInfo; } // This function basically just builds a genesis storage key/value store according to // our desired mock up. - fn new_test_ext() -> sp_io::TestExternalities { + pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - pallet_balances::GenesisConfig::{ + pallet_balances::GenesisConfig:: { balances: vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)], }.assimilate_storage(&mut t).unwrap(); t.into() @@ -1107,652 +469,303 @@ mod tests { #[test] fn basic_setup_works() { - new_test_ext().execute_with(|| { - assert_eq!(Slots::auction_counter(), 0); - assert_eq!(Slots::deposit_held(&0u32.into()), 0); - assert_eq!(Slots::is_in_progress(), false); - assert_eq!(Slots::is_ending(System::block_number()), None); - - run_to_block(10); - - assert_eq!(Slots::auction_counter(), 0); - assert_eq!(Slots::deposit_held(&0u32.into()), 0); - assert_eq!(Slots::is_in_progress(), false); - assert_eq!(Slots::is_ending(System::block_number()), None); - }); - } - - #[test] - fn can_start_auction() { new_test_ext().execute_with(|| { run_to_block(1); + assert_eq!(Slots::lease_period(), 10); + assert_eq!(Slots::lease_period_index(), 0); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - - assert_eq!(Slots::auction_counter(), 1); - assert_eq!(Slots::is_in_progress(), true); - assert_eq!(Slots::is_ending(System::block_number()), None); + run_to_block(10); + assert_eq!(Slots::lease_period_index(), 1); }); } #[test] - fn auction_proceeds_correctly() { + fn lease_lifecycle_works() { new_test_ext().execute_with(|| { run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - - assert_eq!(Slots::auction_counter(), 1); - assert_eq!(Slots::is_in_progress(), true); - assert_eq!(Slots::is_ending(System::block_number()), None); - - run_to_block(2); - assert_eq!(Slots::is_in_progress(), true); - assert_eq!(Slots::is_ending(System::block_number()), None); - - run_to_block(3); - assert_eq!(Slots::is_in_progress(), true); - assert_eq!(Slots::is_ending(System::block_number()), None); - - run_to_block(4); - assert_eq!(Slots::is_in_progress(), true); - assert_eq!(Slots::is_ending(System::block_number()), None); - - run_to_block(5); - assert_eq!(Slots::is_in_progress(), true); - assert_eq!(Slots::is_ending(System::block_number()), None); - - run_to_block(6); - assert_eq!(Slots::is_in_progress(), true); - assert_eq!(Slots::is_ending(System::block_number()), Some(0)); - - run_to_block(7); - assert_eq!(Slots::is_in_progress(), true); - assert_eq!(Slots::is_ending(System::block_number()), Some(1)); + assert_ok!(TestRegistrar::::register(1, ParaId::from(1), Default::default(), Default::default())); - run_to_block(8); - assert_eq!(Slots::is_in_progress(), true); - assert_eq!(Slots::is_ending(System::block_number()), Some(2)); - - run_to_block(9); - assert_eq!(Slots::is_in_progress(), false); - assert_eq!(Slots::is_ending(System::block_number()), None); - }); - } - - #[test] - fn can_win_auction() { - new_test_ext().execute_with(|| { - run_to_block(1); + assert!(Slots::lease_out(1.into(), &1, 1, 1, 1).is_ok()); + assert_eq!(Slots::deposit_held(1.into(), &1), 1); + assert_eq!(Balances::reserved_balance(1), 1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 4, 1)); + run_to_block(19); + assert_eq!(Slots::deposit_held(1.into(), &1), 1); assert_eq!(Balances::reserved_balance(1), 1); - assert_eq!(Balances::free_balance(1), 9); - run_to_block(9); - assert_eq!(Slots::onboard_queue(1), vec![0.into()]); - assert_eq!(Slots::onboarding(ParaId::from(0)), - Some((1, IncomingParachain::Unset(NewBidder { who: 1, sub: 0 }))) - ); - assert_eq!(Slots::deposit_held(&0.into()), 1); + run_to_block(20); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); assert_eq!(Balances::reserved_balance(1), 0); - assert_eq!(Balances::free_balance(1), 9); - }); - } - - #[test] - fn offboarding_works() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 4, 1)); - assert_eq!(Balances::free_balance(1), 9); - - run_to_block(9); - assert_eq!(Slots::deposit_held(&0.into()), 1); - assert_eq!(Slots::deposits(ParaId::from(0))[0], 0); - run_to_block(50); - assert_eq!(Slots::deposit_held(&0.into()), 0); - assert_eq!(Balances::free_balance(1), 10); + assert_eq!(TestRegistrar::::operations(), vec![ + (1.into(), 10, true), + (1.into(), 20, false), + ]); }); } #[test] - fn set_offboarding_works() { + fn lease_interrupted_lifecycle_works() { new_test_ext().execute_with(|| { run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 4, 1)); - run_to_block(9); - assert_eq!(Slots::deposit_held(&0.into()), 1); - assert_eq!(Slots::deposits(ParaId::from(0))[0], 0); + assert_ok!(TestRegistrar::::register(1, ParaId::from(1), Default::default(), Default::default())); - run_to_block(49); - assert_eq!(Slots::deposit_held(&0.into()), 1); - assert_ok!(Slots::set_offboarding(Origin::signed(ParaId::from(0).into_account()), 10)); + assert!(Slots::lease_out(1.into(), &1, 6, 1, 1).is_ok()); + assert!(Slots::lease_out(1.into(), &1, 4, 3, 1).is_ok()); - run_to_block(50); - assert_eq!(Slots::deposit_held(&0.into()), 0); - assert_eq!(Balances::free_balance(10), 1); - }); - } - - #[test] - fn onboarding_works() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 4, 1)); + run_to_block(19); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); - run_to_block(9); - let h = BlakeTwo256::hash(&[42u8][..]); - assert_ok!(Slots::fix_deploy_data(Origin::signed(1), 0, 0.into(), h, 1, vec![69].into())); - assert_ok!(Slots::elaborate_deploy_data(Origin::signed(0), 0.into(), vec![42].into())); + run_to_block(20); + assert_eq!(Slots::deposit_held(1.into(), &1), 4); + assert_eq!(Balances::reserved_balance(1), 4); - run_to_block(10); - with_parachains(|p| { - assert_eq!(p.len(), 1); - assert_eq!(p[&0], (vec![42].into(), vec![69].into())); - }); - }); - } + run_to_block(39); + assert_eq!(Slots::deposit_held(1.into(), &1), 4); + assert_eq!(Balances::reserved_balance(1), 4); - #[test] - fn late_onboarding_works() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 4, 1)); - - run_to_block(10); - with_parachains(|p| { - assert_eq!(p.len(), 0); - }); + run_to_block(40); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); - run_to_block(11); - let h = BlakeTwo256::hash(&[42u8][..]); - assert_ok!(Slots::fix_deploy_data(Origin::signed(1), 0, 0.into(), h, 1, vec![69].into())); - assert_ok!(Slots::elaborate_deploy_data(Origin::signed(0), 0.into(), vec![42].into())); - with_parachains(|p| { - assert_eq!(p.len(), 1); - assert_eq!(p[&0], (vec![42].into(), vec![69].into())); - }); + assert_eq!(TestRegistrar::::operations(), vec![ + (1.into(), 10, true), + (1.into(), 20, false), + (1.into(), 30, true), + (1.into(), 40, false), + ]); }); } #[test] - fn under_bidding_works() { + fn lease_relayed_lifecycle_works() { new_test_ext().execute_with(|| { run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 4, 5)); - assert_ok!(Slots::bid(Origin::signed(2), 0, 1, 1, 4, 1)); - assert_eq!(Balances::reserved_balance(2), 0); - assert_eq!(Balances::free_balance(2), 20); - assert_eq!( - Slots::winning(0).unwrap()[SlotRange::ZeroThree as u8 as usize], - Some((Bidder::New(NewBidder{who: 1, sub: 0}), 5)) - ); - }); - } - #[test] - fn should_choose_best_combination() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 1, 1)); - assert_ok!(Slots::bid(Origin::signed(2), 0, 1, 2, 3, 1)); - assert_ok!(Slots::bid(Origin::signed(3), 0, 1, 4, 4, 2)); - assert_ok!(Slots::bid(Origin::signed(1), 1, 1, 1, 4, 1)); - run_to_block(9); - assert_eq!(Slots::onboard_queue(1), vec![0.into()]); - assert_eq!( - Slots::onboarding(ParaId::from(0)), - Some((1, IncomingParachain::Unset(NewBidder { who: 1, sub: 0 }))) - ); - assert_eq!(Slots::onboard_queue(2), vec![1.into()]); - assert_eq!( - Slots::onboarding(ParaId::from(1)), - Some((2, IncomingParachain::Unset(NewBidder { who: 2, sub: 0 }))) - ); - assert_eq!(Slots::onboard_queue(4), vec![2.into()]); - assert_eq!( - Slots::onboarding(ParaId::from(2)), - Some((4, IncomingParachain::Unset(NewBidder { who: 3, sub: 0 }))) - ); - }); - } + assert_ok!(TestRegistrar::::register(1, ParaId::from(1), Default::default(), Default::default())); - #[test] - fn independent_bids_should_fail() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 1, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 2, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 2, 4, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 2, 2, 1)); - assert_noop!( - Slots::bid(Origin::signed(1), 0, 1, 3, 3, 1), - Error::::NonIntersectingRange - ); - }); - } + assert!(Slots::lease_out(1.into(), &1, 6, 1, 1).is_ok()); + assert!(Slots::lease_out(1.into(), &2, 4, 2, 1).is_ok()); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); + assert_eq!(Slots::deposit_held(1.into(), &2), 4); + assert_eq!(Balances::reserved_balance(2), 4); - #[test] - fn multiple_onboards_offboards_should_work() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 1, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 1, 1)); - assert_ok!(Slots::bid(Origin::signed(2), 0, 1, 2, 3, 1)); - assert_ok!(Slots::bid(Origin::signed(3), 0, 1, 4, 4, 1)); - - run_to_block(5); - assert_ok!(Slots::new_auction(Origin::root(), 1, 1)); - assert_ok!(Slots::bid(Origin::signed(4), 1, 2, 1, 2, 1)); - assert_ok!(Slots::bid(Origin::signed(5), 1, 2, 3, 4, 1)); - - run_to_block(9); - assert_eq!(Slots::onboard_queue(1), vec![0.into(), 3.into()]); - assert_eq!( - Slots::onboarding(ParaId::from(0)), - Some((1, IncomingParachain::Unset(NewBidder { who: 1, sub: 0 }))) - ); - assert_eq!( - Slots::onboarding(ParaId::from(3)), - Some((1, IncomingParachain::Unset(NewBidder { who: 4, sub: 1 }))) - ); - assert_eq!(Slots::onboard_queue(2), vec![1.into()]); - assert_eq!( - Slots::onboarding(ParaId::from(1)), - Some((2, IncomingParachain::Unset(NewBidder { who: 2, sub: 0 }))) - ); - assert_eq!(Slots::onboard_queue(3), vec![4.into()]); - assert_eq!( - Slots::onboarding(ParaId::from(4)), - Some((3, IncomingParachain::Unset(NewBidder { who: 5, sub: 1 }))) - ); - assert_eq!(Slots::onboard_queue(4), vec![2.into()]); - assert_eq!( - Slots::onboarding(ParaId::from(2)), - Some((4, IncomingParachain::Unset(NewBidder { who: 3, sub: 0 }))) - ); + run_to_block(19); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); + assert_eq!(Slots::deposit_held(1.into(), &2), 4); + assert_eq!(Balances::reserved_balance(2), 4); - for &(para, sub, acc) in &[(0, 0, 1), (1, 0, 2), (2, 0, 3), (3, 1, 4), (4, 1, 5)] { - let h = BlakeTwo256::hash(&[acc][..]); - assert_ok!(Slots::fix_deploy_data(Origin::signed(acc as _), sub, para.into(), h, 1, vec![acc].into())); - assert_ok!(Slots::elaborate_deploy_data(Origin::signed(0), para.into(), vec![acc].into())); - } - - run_to_block(10); - with_parachains(|p| { - assert_eq!(p.len(), 2); - assert_eq!(p[&0], (vec![1].into(), vec![1].into())); - assert_eq!(p[&3], (vec![4].into(), vec![4].into())); - }); run_to_block(20); - with_parachains(|p| { - assert_eq!(p.len(), 2); - assert_eq!(p[&1], (vec![2].into(), vec![2].into())); - assert_eq!(p[&3], (vec![4].into(), vec![4].into())); - }); - run_to_block(30); - with_parachains(|p| { - assert_eq!(p.len(), 2); - assert_eq!(p[&1], (vec![2].into(), vec![2].into())); - assert_eq!(p[&4], (vec![5].into(), vec![5].into())); - }); - run_to_block(40); - with_parachains(|p| { - assert_eq!(p.len(), 2); - assert_eq!(p[&2], (vec![3].into(), vec![3].into())); - assert_eq!(p[&4], (vec![5].into(), vec![5].into())); - }); - run_to_block(50); - with_parachains(|p| { - assert_eq!(p.len(), 0); - }); - }); - } - - #[test] - fn extensions_should_work() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 1, 1)); - - run_to_block(9); - assert_eq!(Slots::onboard_queue(1), vec![0.into()]); - - run_to_block(10); - let h = BlakeTwo256::hash(&[1u8][..]); - assert_ok!(Slots::fix_deploy_data(Origin::signed(1), 0, 0.into(), h, 1, vec![1].into())); - assert_ok!(Slots::elaborate_deploy_data(Origin::signed(0), 0.into(), vec![1].into())); - - assert_ok!(Slots::new_auction(Origin::root(), 5, 2)); - assert_ok!(Slots::bid_renew(Origin::signed(ParaId::from(0).into_account()), 2, 2, 2, 1)); - - with_parachains(|p| { - assert_eq!(p.len(), 1); - assert_eq!(p[&0], (vec![1].into(), vec![1].into())); - }); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); + assert_eq!(Slots::deposit_held(1.into(), &2), 4); + assert_eq!(Balances::reserved_balance(2), 4); - run_to_block(20); - with_parachains(|p| { - assert_eq!(p.len(), 1); - assert_eq!(p[&0], (vec![1].into(), vec![1].into())); - }); - assert_ok!(Slots::new_auction(Origin::root(), 5, 2)); - assert_ok!(Balances::transfer(Origin::signed(1), ParaId::from(0).into_account(), 1)); - assert_ok!(Slots::bid_renew(Origin::signed(ParaId::from(0).into_account()), 3, 3, 3, 2)); + run_to_block(29); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); + assert_eq!(Slots::deposit_held(1.into(), &2), 4); + assert_eq!(Balances::reserved_balance(2), 4); run_to_block(30); - with_parachains(|p| { - assert_eq!(p.len(), 1); - assert_eq!(p[&0], (vec![1].into(), vec![1].into())); - }); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); + assert_eq!(Slots::deposit_held(1.into(), &2), 0); + assert_eq!(Balances::reserved_balance(2), 0); - run_to_block(40); - with_parachains(|p| { - assert_eq!(p.len(), 0); - }); + assert_eq!(TestRegistrar::::operations(), vec![ + (1.into(), 10, true), + (1.into(), 30, false), + ]); }); } #[test] - fn renewal_with_lower_value_should_work() { + fn lease_deposit_increase_works() { new_test_ext().execute_with(|| { run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 1, 5)); - run_to_block(9); - assert_eq!(Slots::onboard_queue(1), vec![0.into()]); + assert_ok!(TestRegistrar::::register(1, ParaId::from(1), Default::default(), Default::default())); - run_to_block(10); - let h = BlakeTwo256::hash(&[1u8][..]); - assert_ok!(Slots::fix_deploy_data(Origin::signed(1), 0, 0.into(), h, 1, vec![1].into())); - assert_ok!(Slots::elaborate_deploy_data(Origin::signed(0), 0.into(), vec![1].into())); - - assert_ok!(Slots::new_auction(Origin::root(), 5, 2)); - assert_ok!(Slots::bid_renew(Origin::signed(ParaId::from(0).into_account()), 2, 2, 2, 3)); + assert!(Slots::lease_out(1.into(), &1, 4, 1, 1).is_ok()); + assert_eq!(Slots::deposit_held(1.into(), &1), 4); + assert_eq!(Balances::reserved_balance(1), 4); - run_to_block(20); - assert_eq!(Balances::free_balance(&ParaId::from(0u32).into_account()), 2); + assert!(Slots::lease_out(1.into(), &1, 6, 2, 1).is_ok()); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); - assert_ok!(Slots::new_auction(Origin::root(), 5, 2)); - assert_ok!(Slots::bid_renew(Origin::signed(ParaId::from(0).into_account()), 3, 3, 3, 4)); + run_to_block(29); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); run_to_block(30); - assert_eq!(Balances::free_balance(&ParaId::from(0u32).into_account()), 1); - }); - } - - #[test] - fn can_win_incomplete_auction() { - new_test_ext().execute_with(|| { - run_to_block(1); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 4, 4, 5)); - - run_to_block(9); - assert_eq!(Slots::onboard_queue(1), vec![]); - assert_eq!(Slots::onboard_queue(2), vec![]); - assert_eq!(Slots::onboard_queue(3), vec![]); - assert_eq!(Slots::onboard_queue(4), vec![0.into()]); - assert_eq!( - Slots::onboarding(ParaId::from(0)), - Some((4, IncomingParachain::Unset(NewBidder { who: 1, sub: 0 }))) - ); - assert_eq!(Slots::deposit_held(&0.into()), 5); + assert_eq!(TestRegistrar::::operations(), vec![ + (1.into(), 10, true), + (1.into(), 30, false), + ]); }); } #[test] - fn multiple_bids_work_pre_ending() { + fn lease_deposit_decrease_works() { new_test_ext().execute_with(|| { run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - - for i in 1..6u64 { - run_to_block(i as _); - assert_ok!(Slots::bid(Origin::signed(i), 0, 1, 1, 4, i)); - for j in 1..6 { - assert_eq!(Balances::reserved_balance(j), if j == i { j } else { 0 }); - assert_eq!(Balances::free_balance(j), if j == i { j * 9 } else { j * 10 }); - } - } + assert_ok!(TestRegistrar::::register(1, ParaId::from(1), Default::default(), Default::default())); - run_to_block(9); - assert_eq!(Slots::onboard_queue(1), vec![0.into()]); - assert_eq!( - Slots::onboarding(ParaId::from(0)), - Some((1, IncomingParachain::Unset(NewBidder { who: 5, sub: 0 }))) - ); - assert_eq!(Slots::deposit_held(&0.into()), 5); - assert_eq!(Balances::reserved_balance(5), 0); - assert_eq!(Balances::free_balance(5), 45); - }); - } + assert!(Slots::lease_out(1.into(), &1, 6, 1, 1).is_ok()); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); - #[test] - fn multiple_bids_work_post_ending() { - new_test_ext().execute_with(|| { - run_to_block(1); + assert!(Slots::lease_out(1.into(), &1, 4, 2, 1).is_ok()); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); + run_to_block(19); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); - for i in 1..6u64 { - run_to_block((i + 3) as _); - assert_ok!(Slots::bid(Origin::signed(i), 0, 1, 1, 4, i)); - for j in 1..6 { - assert_eq!(Balances::reserved_balance(j), if j == i { j } else { 0 }); - assert_eq!(Balances::free_balance(j), if j == i { j * 9 } else { j * 10 }); - } - } - - run_to_block(9); - assert_eq!(Slots::onboard_queue(1), vec![0.into()]); - assert_eq!( - Slots::onboarding(ParaId::from(0)), - Some((1, IncomingParachain::Unset(NewBidder { who: 3, sub: 0 }))) - ); - assert_eq!(Slots::deposit_held(&0.into()), 3); - assert_eq!(Balances::reserved_balance(3), 0); - assert_eq!(Balances::free_balance(3), 27); - }); - } + run_to_block(20); + assert_eq!(Slots::deposit_held(1.into(), &1), 4); + assert_eq!(Balances::reserved_balance(1), 4); - #[test] - fn incomplete_calculate_winners_works() { - let winning = [ - None, - None, - None, - None, - None, - None, - None, - None, - None, - Some((Bidder::New(NewBidder{who: 1, sub: 0}), 1)), - ]; - let winners = vec![ - (Some(NewBidder{who: 1, sub: 0}), 0.into(), 1, SlotRange::ThreeThree) - ]; - - assert_eq!(Slots::calculate_winners(winning, TestParachains::new_id), winners); - } + run_to_block(29); + assert_eq!(Slots::deposit_held(1.into(), &1), 4); + assert_eq!(Balances::reserved_balance(1), 4); - #[test] - fn first_incomplete_calculate_winners_works() { - let winning = [ - Some((Bidder::New(NewBidder{who: 1, sub: 0}), 1)), - None, - None, - None, - None, - None, - None, - None, - None, - None, - ]; - let winners = vec![ - (Some(NewBidder{who: 1, sub: 0}), 0.into(), 1, SlotRange::ZeroZero) - ]; - - assert_eq!(Slots::calculate_winners(winning, TestParachains::new_id), winners); - } + run_to_block(30); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); - #[test] - fn calculate_winners_works() { - let mut winning = [ - /*0..0*/ - Some((Bidder::New(NewBidder{who: 2, sub: 0}), 2)), - /*0..1*/ - None, - /*0..2*/ - None, - /*0..3*/ - Some((Bidder::New(NewBidder{who: 1, sub: 0}), 1)), - /*1..1*/ - Some((Bidder::New(NewBidder{who: 3, sub: 0}), 1)), - /*1..2*/ - None, - /*1..3*/ - None, - /*2..2*/ - //Some((Bidder::New(NewBidder{who: 4, sub: 0}), 1)), - Some((Bidder::New(NewBidder{who: 1, sub: 0}), 53)), - /*2..3*/ - None, - /*3..3*/ - Some((Bidder::New(NewBidder{who: 5, sub: 0}), 1)), - ]; - let winners = vec![ - (Some(NewBidder{who: 2,sub: 0}), 0.into(), 2, SlotRange::ZeroZero), - (Some(NewBidder{who: 3,sub: 0}), 1.into(), 1, SlotRange::OneOne), - (Some(NewBidder{who: 1,sub: 0}), 2.into(), 53, SlotRange::TwoTwo), - (Some(NewBidder{who: 5,sub: 0}), 3.into(), 1, SlotRange::ThreeThree) - ]; - - assert_eq!(Slots::calculate_winners(winning.clone(), TestParachains::new_id), winners); - - reset_count(); - winning[SlotRange::ZeroThree as u8 as usize] = Some((Bidder::New(NewBidder{who: 1, sub: 0}), 2)); - let winners = vec![ - (Some(NewBidder{who: 2,sub: 0}), 0.into(), 2, SlotRange::ZeroZero), - (Some(NewBidder{who: 3,sub: 0}), 1.into(), 1, SlotRange::OneOne), - (Some(NewBidder{who: 1,sub: 0}), 2.into(), 53, SlotRange::TwoTwo), - (Some(NewBidder{who: 5,sub: 0}), 3.into(), 1, SlotRange::ThreeThree) - ]; - assert_eq!(Slots::calculate_winners(winning.clone(), TestParachains::new_id), winners); - - reset_count(); - winning[SlotRange::ZeroOne as u8 as usize] = Some((Bidder::New(NewBidder{who: 4, sub: 0}), 3)); - let winners = vec![ - (Some(NewBidder{who: 4,sub: 0}), 0.into(), 3, SlotRange::ZeroOne), - (Some(NewBidder{who: 1,sub: 0}), 1.into(), 53, SlotRange::TwoTwo), - (Some(NewBidder{who: 5,sub: 0}), 2.into(), 1, SlotRange::ThreeThree) - ]; - assert_eq!(Slots::calculate_winners(winning.clone(), TestParachains::new_id), winners); + assert_eq!(TestRegistrar::::operations(), vec![ + (1.into(), 10, true), + (1.into(), 30, false), + ]); + }); } +} - #[test] - fn deploy_code_too_large() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 1, 5)); - - run_to_block(9); - assert_eq!(Slots::onboard_queue(1), vec![0.into()]); - - run_to_block(10); +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking { + use super::*; + use frame_system::RawOrigin; + use frame_support::assert_ok; + use sp_runtime::traits::Bounded; + + use frame_benchmarking::{benchmarks, account}; + + use crate::slots::Module as Slots; + + fn assert_last_event(generic_event: ::Event) { + let events = frame_system::Module::::events(); + let system_event: ::Event = generic_event.into(); + // compare to the last event record + let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); + } + + fn register_a_parathread(i: u32) -> (ParaId, T::AccountId) { + let para = ParaId::from(i); + let leaser: T::AccountId = account("leaser", i, 0); + T::Currency::make_free_balance_be(&leaser, BalanceOf::::max_value()); + let worst_head_data = T::Registrar::worst_head_data(); + let worst_validation_code = T::Registrar::worst_validation_code(); + + assert_ok!(T::Registrar::register(leaser.clone(), para, worst_head_data, worst_validation_code)); + T::Registrar::execute_pending_transitions(); + + (para, leaser) + } + + benchmarks! { + force_lease { + let para = ParaId::from(1337); + let leaser: T::AccountId = account("leaser", 0, 0); + T::Currency::make_free_balance_be(&leaser, BalanceOf::::max_value()); + let amount = T::Currency::minimum_balance(); + let period_begin = 0u32.into(); + let period_count = 3u32.into(); + }: _(RawOrigin::Root, para, leaser.clone(), amount, period_begin, period_count) + verify { + assert_last_event::(RawEvent::Leased(para, leaser, period_begin, period_count, amount, amount).into()); + } - let code = vec![0u8; (MAX_CODE_SIZE + 1) as _]; - let h = BlakeTwo256::hash(&code[..]); - assert_eq!( - Slots::fix_deploy_data( - Origin::signed(1), 0, 0.into(), h, code.len() as _, vec![1].into(), - ), - Err(Error::::CodeTooLarge.into()), - ); - }); - } + // Worst case scenario, T parathreads onboard, and C parachains offboard. + manage_lease_period_start { + // Assume reasonable maximum of 100 paras at any time + let c in 1 .. 100; + let t in 1 .. 100; - #[test] - fn deploy_maximum_ok() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 1, 5)); + let period_begin = 0u32.into(); + let period_count = 3u32.into(); - run_to_block(9); - assert_eq!(Slots::onboard_queue(1), vec![0.into()]); + // T parathread are upgrading to parachains + for i in 0 .. t { + let (para, leaser) = register_a_parathread::(i); + let amount = T::Currency::minimum_balance(); - run_to_block(10); + Slots::::force_lease(RawOrigin::Root.into(), para, leaser, amount, period_begin, period_count)?; + } - let code = vec![0u8; MAX_CODE_SIZE as _]; - let head_data = vec![1u8; MAX_HEAD_DATA_SIZE as _].into(); - let h = BlakeTwo256::hash(&code[..]); - assert_ok!(Slots::fix_deploy_data( - Origin::signed(1), 0, 0.into(), h, code.len() as _, head_data, - )); - }); - } + T::Registrar::execute_pending_transitions(); - #[test] - fn deploy_head_data_too_large() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 1, 5)); + // C parachains are downgrading to parathreads + for i in 200 .. 200 + c { + let (para, leaser) = register_a_parathread::(i); + T::Registrar::make_parachain(para)?; + } - run_to_block(9); - assert_eq!(Slots::onboard_queue(1), vec![0.into()]); + T::Registrar::execute_pending_transitions(); - run_to_block(10); + for i in 0 .. t { + assert!(T::Registrar::is_parathread(ParaId::from(i))); + } - let code = vec![0u8; MAX_CODE_SIZE as _]; - let head_data = vec![1u8; (MAX_HEAD_DATA_SIZE + 1) as _].into(); - let h = BlakeTwo256::hash(&code[..]); - assert_eq!( - Slots::fix_deploy_data( - Origin::signed(1), 0, 0.into(), h, code.len() as _, head_data, - ), - Err(Error::::HeadDataTooLarge.into()), - ); - }); + for i in 200 .. 200 + c { + assert!(T::Registrar::is_parachain(ParaId::from(i))); + } + }: { + Slots::::manage_lease_period_start(period_begin); + } verify { + // All paras should have switched. + T::Registrar::execute_pending_transitions(); + for i in 0 .. t { + assert!(T::Registrar::is_parachain(ParaId::from(i))); + } + for i in 200 .. 200 + c { + assert!(T::Registrar::is_parathread(ParaId::from(i))); + } + } } - #[test] - fn code_size_must_be_correct() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Slots::bid(Origin::signed(1), 0, 1, 1, 1, 5)); - - run_to_block(9); - assert_eq!(Slots::onboard_queue(1), vec![0.into()]); + #[cfg(test)] + mod tests { + use super::*; + use crate::integration_tests::{new_test_ext, Test}; + use frame_support::assert_ok; - run_to_block(10); + #[test] + fn force_lease_benchmark() { + new_test_ext().execute_with(|| { + assert_ok!(test_benchmark_force_lease::()); + }); + } - let code = vec![0u8; MAX_CODE_SIZE as _]; - let head_data = vec![1u8; MAX_HEAD_DATA_SIZE as _].into(); - let h = BlakeTwo256::hash(&code[..]); - assert_ok!(Slots::fix_deploy_data( - Origin::signed(1), 0, 0.into(), h, (code.len() - 1) as _, head_data, - )); - assert!(Slots::elaborate_deploy_data(Origin::signed(0), 0.into(), code.into()).is_err()); - }); + #[test] + fn manage_lease_period_start_benchmark() { + new_test_ext().execute_with(|| { + assert_ok!(test_benchmark_manage_lease_period_start::()); + }); + } } } diff --git a/runtime/common/src/traits.rs b/runtime/common/src/traits.rs new file mode 100644 index 000000000000..97edf65adb48 --- /dev/null +++ b/runtime/common/src/traits.rs @@ -0,0 +1,182 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . + +//! Traits used across pallets for Polkadot. + +use sp_std::vec::*; +use primitives::v1::{HeadData, ValidationCode, Id as ParaId}; +use frame_support::{ + dispatch::DispatchResult, + traits::{Currency, ReservableCurrency}, +}; + +/// Parachain registration API. +pub trait Registrar { + /// The account ID type that encodes a parachain manager ID. + type AccountId; + + /// Report the manager (permissioned owner) of a parachain, if there is one. + fn manager_of(id: ParaId) -> Option; + + /// All parachains. Ordered ascending by ParaId. Parathreads are not included. + fn parachains() -> Vec; + + /// Return if a ParaId is a Parachain. + fn is_parachain(id: ParaId) -> bool { + Self::parachains().binary_search(&id).is_ok() + } + + /// Return if a ParaId is a Parathread. + fn is_parathread(id: ParaId) -> bool; + + /// Return if a ParaId is registered in the system. + fn is_registered(id: ParaId) -> bool { + Self::is_parathread(id) || Self::is_parachain(id) + } + + // Register a Para ID under control of `who`. + fn register( + who: Self::AccountId, + id: ParaId, + genesis_head: HeadData, + validation_code: ValidationCode, + ) -> DispatchResult; + + // Deregister a Para ID, free any data, and return any deposits. + fn deregister(id: ParaId) -> DispatchResult; + + /// Elevate a para to parachain status. + fn make_parachain(id: ParaId) -> DispatchResult; + + /// Lower a para back to normal from parachain status. + fn make_parathread(id: ParaId) -> DispatchResult; + + #[cfg(any(feature = "runtime-benchmarks", test))] + fn worst_head_data() -> HeadData; + + #[cfg(any(feature = "runtime-benchmarks", test))] + fn worst_validation_code() -> ValidationCode; + + #[cfg(any(feature = "runtime-benchmarks", test))] + fn execute_pending_transitions(); +} + +/// Error type for something that went wrong with leasing. +pub enum LeaseError { + /// Unable to reserve the funds in the leaser's account. + ReserveFailed, + /// There is already a lease on at least one period for the given para. + AlreadyLeased, + /// The period to be leased has already ended. + AlreadyEnded, +} + +/// Lease manager. Used by the auction module to handle parachain slot leases. +pub trait Leaser { + /// An account identifier for a leaser. + type AccountId; + + /// The measurement type for counting lease periods (generally just a `BlockNumber`). + type LeasePeriod; + + /// The currency type in which the lease is taken. + type Currency: ReservableCurrency; + + /// Lease a new parachain slot for `para`. + /// + /// `leaser` shall have a total of `amount` balance reserved by the implementor of this trait. + /// + /// Note: The implementor of the trait (the leasing system) is expected to do all reserve/unreserve calls. The + /// caller of this trait *SHOULD NOT* pre-reserve the deposit (though should ensure that it is reservable). + /// + /// The lease will last from `period_begin` for `period_count` lease periods. It is undefined if the `para` + /// already has a slot leased during those periods. + /// + /// Returns `Err` in the case of an error, and in which case nothing is changed. + fn lease_out( + para: ParaId, + leaser: &Self::AccountId, + amount: >::Balance, + period_begin: Self::LeasePeriod, + period_count: Self::LeasePeriod, + ) -> Result<(), LeaseError>; + + /// Return the amount of balance currently held in reserve on `leaser`'s account for leasing `para`. This won't + /// go down outside of a lease period. + fn deposit_held(para: ParaId, leaser: &Self::AccountId) -> >::Balance; + + /// The lease period. This is constant, but can't be a `const` due to it being a runtime configurable quantity. + fn lease_period() -> Self::LeasePeriod; + + /// Returns the current lease period. + fn lease_period_index() -> Self::LeasePeriod; +} + +pub trait Auctioneer { + /// An account identifier for a leaser. + type AccountId; + + /// The measurement type for counting blocks. + type BlockNumber; + + /// The measurement type for counting lease periods (generally the same as `BlockNumber`). + type LeasePeriod; + + /// The currency type in which the lease is taken. + type Currency: ReservableCurrency; + + /// Create a new auction. + /// + /// This can only happen when there isn't already an auction in progress. Accepts the `duration` + /// of this auction and the `lease_period_index` of the initial lease period of the four that + /// are to be auctioned. + fn new_auction(duration: Self::BlockNumber, lease_period_index: Self::LeasePeriod) -> DispatchResult; + + /// Returns `Some(n)` if the `now` block is part of the ending period of an auction, where `n` + /// represents how far into the ending period this block is. Otherwise, returns `None`. + fn is_ending(now: Self::BlockNumber) -> Option; + + /// Place a bid in the current auction. + /// + /// - `bidder`: The account that will be funding this bid. + /// - `para`: The para to bid for. + /// - `first_slot`: The first lease period index of the range to be bid on. + /// - `last_slot`: The last lease period index of the range to be bid on (inclusive). + /// - `amount`: The total amount to be the bid for deposit over the range. + /// + /// The account `Bidder` must have at least `amount` available as a free balance in `Currency`. The + /// implementation *MUST* remove or reserve `amount` funds from `bidder` and those funds should be returned + /// or freed once the bid is rejected or lease has ended. + fn place_bid( + bidder: Self::AccountId, + para: ParaId, + first_slot: Self::LeasePeriod, + last_slot: Self::LeasePeriod, + amount: >::Balance, + ) -> DispatchResult; + + /// Returns the current lease period. + fn lease_period_index() -> Self::LeasePeriod; +} + +/// Runtime hook for when we swap a parachain and parathread. +#[impl_trait_for_tuples::impl_for_tuples(30)] +pub trait OnSwap { + /// Updates any needed state/references to enact a logical swap of two parachains. Identity, + /// code and `head_data` remain equivalent for all parachains/threads, however other properties + /// such as leases, deposits held and thread/chain nature are swapped. + fn on_swap(one: ParaId, other: ParaId); +} diff --git a/runtime/parachains/src/lib.rs b/runtime/parachains/src/lib.rs index 8de3841cc474..9e140d5f58a0 100644 --- a/runtime/parachains/src/lib.rs +++ b/runtime/parachains/src/lib.rs @@ -44,11 +44,12 @@ mod util; mod mock; pub use origin::{Origin, ensure_parachain}; +use primitives::v1::Id as ParaId; pub use paras::ParaLifecycle; /// Schedule a para to be initialized at the start of the next session with the given genesis data. pub fn schedule_para_initialize( - id: primitives::v1::Id, + id: ParaId, genesis: paras::ParaGenesisArgs, ) -> Result<(), ()> { >::schedule_para_initialize(id, genesis).map_err(|_| ()) @@ -58,3 +59,13 @@ pub fn schedule_para_initialize( pub fn schedule_para_cleanup(id: primitives::v1::Id) -> Result<(), ()> { >::schedule_para_cleanup(id).map_err(|_| ()) } + +/// Schedule a parathread to be upgraded to a parachain. +pub fn schedule_parathread_upgrade(id: ParaId) -> Result<(), ()> { + paras::Module::::schedule_parathread_upgrade(id).map_err(|_| ()) +} + +/// Schedule a parachain to be downgraded to a parathread. +pub fn schedule_parachain_downgrade(id: ParaId) -> Result<(), ()> { + paras::Module::::schedule_parachain_downgrade(id).map_err(|_| ()) +} diff --git a/runtime/parachains/src/paras.rs b/runtime/parachains/src/paras.rs index b6f1b6166cf8..3a6b741600f5 100644 --- a/runtime/parachains/src/paras.rs +++ b/runtime/parachains/src/paras.rs @@ -753,6 +753,15 @@ impl Module { fn scheduled_session() -> SessionIndex { shared::Module::::scheduled_session() } + + /// Test function for triggering a new session in this pallet. + #[cfg(any(feature = "std", feature = "runtime-benchmarks", test))] + pub fn test_on_new_session() { + Self::initializer_on_new_session(&SessionChangeNotification { + session_index: shared::Module::::session_index(), + ..Default::default() + }); + } } #[cfg(test)] diff --git a/runtime/parachains/src/shared.rs b/runtime/parachains/src/shared.rs index a2eb2548af1b..2cc51b1368e8 100644 --- a/runtime/parachains/src/shared.rs +++ b/runtime/parachains/src/shared.rs @@ -106,12 +106,13 @@ impl Module { } /// Return the session index that should be used for any future scheduled changes. - pub (crate) fn scheduled_session() -> SessionIndex { + pub fn scheduled_session() -> SessionIndex { Self::session_index().saturating_add(SESSION_DELAY) } - #[cfg(test)] - pub(crate) fn set_session_index(index: SessionIndex) { + /// Test function for setting the current session index. + #[cfg(any(feature = "std", feature = "runtime-benchmarks", test))] + pub fn set_session_index(index: SessionIndex) { CurrentSessionIndex::set(index); } diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index c8e0cc280c2a..35f1942d83b6 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -208,7 +208,7 @@ construct_runtime! { Hrmp: parachains_hrmp::{Module, Call, Storage, Event}, SessionInfo: parachains_session_info::{Module, Call, Storage}, - Registrar: paras_registrar::{Module, Call, Storage}, + Registrar: paras_registrar::{Module, Call, Storage, Event}, ParasSudoWrapper: paras_sudo_wrapper::{Module, Call}, // Sudo @@ -257,8 +257,6 @@ impl frame_system::Config for Runtime { } parameter_types! { - pub const MaxCodeSize: u32 = 10 * 1024 * 1024; // 10 MB - pub const MaxHeadDataSize: u32 = 20 * 1024; // 20 KB pub const ValidationUpgradeFrequency: BlockNumber = 2 * DAYS; pub const ValidationUpgradeDelay: BlockNumber = 8 * HOURS; pub const SlashPeriod: BlockNumber = 7 * DAYS; @@ -367,7 +365,6 @@ impl frame_system::offchain::SendTransactionTypes for Runtime where } parameter_types! { - pub const ParathreadDeposit: Balance = 5 * DOLLARS; pub const QueueSize: usize = 2; pub const MaxRetries: u32 = 3; } @@ -594,10 +591,23 @@ impl parachains_initializer::Config for Runtime { impl paras_sudo_wrapper::Config for Runtime {} +parameter_types! { + pub const ParaDeposit: Balance = 5 * DOLLARS; + pub const DataDepositPerByte: Balance = deposit(0, 1); + pub const MaxCodeSize: u32 = 10 * 1024 * 1024; // 10 MB + pub const MaxHeadSize: u32 = 20 * 1024; // 20 KB +} + impl paras_registrar::Config for Runtime { - type Currency = Balances; - type ParathreadDeposit = ParathreadDeposit; + type Event = Event; type Origin = Origin; + type Currency = Balances; + type OnSwap = (); + type ParaDeposit = ParaDeposit; + type DataDepositPerByte = DataDepositPerByte; + type MaxCodeSize = MaxCodeSize; + type MaxHeadSize = MaxHeadSize; + type WeightInfo = paras_registrar::TestWeightInfo; } impl pallet_sudo::Config for Runtime { diff --git a/runtime/rococo/src/propose_parachain.rs b/runtime/rococo/src/propose_parachain.rs index 79f9947141a1..b68b1ece943d 100644 --- a/runtime/rococo/src/propose_parachain.rs +++ b/runtime/rococo/src/propose_parachain.rs @@ -53,12 +53,8 @@ type BalanceOf = ::Balance; /// Configuration for the parachain proposer. pub trait Config: pallet_session::Config - + pallet_balances::Config + pallet_balances::Config + runtime_parachains::paras::Config - + runtime_parachains::dmp::Config - + runtime_parachains::ump::Config - + runtime_parachains::hrmp::Config { /// The overreaching event type. type Event: From> + Into<::Event>; diff --git a/runtime/westend/Cargo.toml b/runtime/westend/Cargo.toml index 4cbdee4db08b..16a2308d9531 100644 --- a/runtime/westend/Cargo.toml +++ b/runtime/westend/Cargo.toml @@ -76,6 +76,7 @@ hex-literal = { version = "0.3.1", optional = true } runtime-common = { package = "polkadot-runtime-common", path = "../common", default-features = false } primitives = { package = "polkadot-primitives", path = "../../primitives", default-features = false } polkadot-parachain = { path = "../../parachain", default-features = false } +runtime-parachains = { package = "polkadot-runtime-parachains", path = "../parachains", default-features = false } [dev-dependencies] hex-literal = "0.3.1" diff --git a/runtime/westend/src/lib.rs b/runtime/westend/src/lib.rs index 875d44804df8..a5a75ccdb092 100644 --- a/runtime/westend/src/lib.rs +++ b/runtime/westend/src/lib.rs @@ -31,14 +31,14 @@ use primitives::v1::{ InboundDownwardMessage, InboundHrmpMessage, SessionInfo, }; use runtime_common::{ + paras_sudo_wrapper, paras_registrar, auctions, crowdloan, slots, SlowAdjustingFeeUpdate, CurrencyToVote, impls::ToAuthor, BlockHashCount, BlockWeights, BlockLength, RocksDbWeight, OffchainSolutionWeightLimit, - ParachainSessionKeyPlaceholder, AssignmentSessionKeyPlaceholder, }; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, - ApplyExtrinsicResult, KeyTypeId, Perbill, curve::PiecewiseLinear, + ApplyExtrinsicResult, KeyTypeId, Perbill, curve::PiecewiseLinear, ModuleId, transaction_validity::{TransactionValidity, TransactionSource, TransactionPriority}, traits::{ BlakeTwo256, Block as BlockT, OpaqueKeys, ConvertInto, AccountIdLookup, @@ -71,6 +71,19 @@ pub use sp_runtime::BuildStorage; pub use pallet_timestamp::Call as TimestampCall; pub use pallet_balances::Call as BalancesCall; +use runtime_parachains::origin as parachains_origin; +use runtime_parachains::configuration as parachains_configuration; +use runtime_parachains::shared as parachains_shared; +use runtime_parachains::inclusion as parachains_inclusion; +use runtime_parachains::inclusion_inherent as parachains_inclusion_inherent; +use runtime_parachains::initializer as parachains_initializer; +use runtime_parachains::session_info as parachains_session_info; +use runtime_parachains::paras as parachains_paras; +use runtime_parachains::dmp as parachains_dmp; +use runtime_parachains::ump as parachains_ump; +use runtime_parachains::hrmp as parachains_hrmp; +use runtime_parachains::scheduler as parachains_scheduler; + /// Constant values used within the runtime. pub mod constants; use constants::{time::*, currency::*, fee::*}; @@ -269,8 +282,8 @@ impl_opaque_keys! { pub grandpa: Grandpa, pub babe: Babe, pub im_online: ImOnline, - pub para_validator: ParachainSessionKeyPlaceholder, - pub para_assignment: AssignmentSessionKeyPlaceholder, + pub para_validator: Initializer, + pub para_assignment: ParachainsSessionInfo, pub authority_discovery: AuthorityDiscovery, } } @@ -693,6 +706,118 @@ impl pallet_proxy::Config for Runtime { type AnnouncementDepositFactor = AnnouncementDepositFactor; } +impl parachains_session_info::Config for Runtime {} + +impl parachains_ump::Config for Runtime { + type UmpSink = (); +} + +impl parachains_dmp::Config for Runtime {} + +impl parachains_hrmp::Config for Runtime { + type Origin = Origin; + type Event = Event; + type Currency = Balances; +} + +impl parachains_inclusion_inherent::Config for Runtime {} + +impl parachains_scheduler::Config for Runtime {} + +impl parachains_initializer::Config for Runtime { + type Randomness = pallet_babe::RandomnessFromOneEpochAgo; +} + +impl paras_sudo_wrapper::Config for Runtime {} + +impl parachains_origin::Config for Runtime {} + +impl parachains_configuration::Config for Runtime {} + +impl parachains_shared::Config for Runtime {} + +/// Special `RewardValidators` that does nothing ;) +pub struct RewardValidators; +impl runtime_parachains::inclusion::RewardValidators for RewardValidators { + fn reward_backing(_: impl IntoIterator) {} + fn reward_bitfields(_: impl IntoIterator) {} +} + +impl parachains_inclusion::Config for Runtime { + type Event = Event; + type RewardValidators = RewardValidators; +} + +impl parachains_paras::Config for Runtime { + type Origin = Origin; +} + +parameter_types! { + pub const ParaDeposit: Balance = 5 * DOLLARS; + pub const DataDepositPerByte: Balance = deposit(0, 1); + pub const MaxCodeSize: u32 = 10 * 1024 * 1024; // 10 MB + pub const MaxHeadSize: u32 = 20 * 1024; // 20 KB +} + +impl paras_registrar::Config for Runtime { + type Event = Event; + type Origin = Origin; + type Currency = Balances; + type OnSwap = (Crowdloan, Slots); + type ParaDeposit = ParaDeposit; + type DataDepositPerByte = DataDepositPerByte; + type MaxCodeSize = MaxCodeSize; + type MaxHeadSize = MaxHeadSize; + type WeightInfo = paras_registrar::TestWeightInfo; +} + +parameter_types! { + pub const EndingPeriod: BlockNumber = 1 * HOURS; +} + +impl auctions::Config for Runtime { + type Event = Event; + type Leaser = Slots; + type EndingPeriod = EndingPeriod; + type Randomness = pallet_babe::RandomnessFromOneEpochAgo; + type InitiateOrigin = EnsureRoot; + type WeightInfo = auctions::TestWeightInfo; +} + +parameter_types! { + pub const LeasePeriod: BlockNumber = 365 * DAYS; +} + +impl slots::Config for Runtime { + type Event = Event; + type Currency = Balances; + type Registrar = Registrar; + type LeasePeriod = LeasePeriod; + type WeightInfo = slots::TestWeightInfo; +} + +parameter_types! { + pub const CrowdloanId: ModuleId = ModuleId(*b"py/cfund"); + pub const SubmissionDeposit: Balance = 1_000 * DOLLARS; + pub const MinContribution: Balance = 100 * DOLLARS; + pub const RetirementPeriod: BlockNumber = 7 * DAYS; + pub const RemoveKeysLimit: u32 = 500; + +} + +impl crowdloan::Config for Runtime { + type Event = Event; + type ModuleId = CrowdloanId; + type SubmissionDeposit = SubmissionDeposit; + type MinContribution = MinContribution; + type RetirementPeriod = RetirementPeriod; + type OrphanedFunds = (); + type RemoveKeysLimit = RemoveKeysLimit; + type Registrar = Registrar; + type Auctioneer = Auctions; + type WeightInfo = crowdloan::TestWeightInfo; +} + construct_runtime! { pub enum Runtime where Block = Block, @@ -747,6 +872,26 @@ construct_runtime! { // Election pallet. Only works with staking, but placed here to maintain indices. ElectionProviderMultiPhase: pallet_election_provider_multi_phase::{Module, Call, Storage, Event, ValidateUnsigned} = 24, + + // Parachains Runtime + ParachainsOrigin: parachains_origin::{Module, Origin} = 28, + ParachainsConfiguration: parachains_configuration::{Module, Call, Storage, Config} = 29, + Shared: parachains_shared::{Module, Call, Storage} = 30, + Inclusion: parachains_inclusion::{Module, Call, Storage, Event} = 31, + InclusionInherent: parachains_inclusion_inherent::{Module, Call, Storage, Inherent} = 32, + ParachainsScheduler: parachains_scheduler::{Module, Call, Storage} = 33, + Paras: parachains_paras::{Module, Call, Storage} = 34, + Initializer: parachains_initializer::{Module, Call, Storage} = 35, + Dmp: parachains_dmp::{Module, Call, Storage} = 36, + Ump: parachains_ump::{Module, Call, Storage} = 37, + Hrmp: parachains_hrmp::{Module, Call, Storage, Event} = 38, + ParachainsSessionInfo: parachains_session_info::{Module, Call, Storage} = 39, + + // Parachain Onboarding Pallets + Registrar: paras_registrar::{Module, Call, Storage, Event} = 40, + Auctions: auctions::{Module, Call, Storage, Event} = 41, + Crowdloan: crowdloan::{Module, Call, Storage, Event} = 42, + Slots: slots::{Module, Call, Storage, Event} = 43, } } @@ -1112,6 +1257,12 @@ sp_api::impl_runtime_apis! { add_benchmark!(params, batches, pallet_utility, Utility); add_benchmark!(params, batches, pallet_vesting, Vesting); + // Polkadot Parachain Benchmarks + add_benchmark!(params, batches, auctions, Auctions); + add_benchmark!(params, batches, crowdloan, Crowdloan); + add_benchmark!(params, batches, paras_registrar, Registrar); + add_benchmark!(params, batches, slots, Slots); + if batches.is_empty() { return Err("Benchmark not found for this pallet.".into()) } Ok(batches) } diff --git a/runtime/westend/src/weights/auctions.rs b/runtime/westend/src/weights/auctions.rs new file mode 100644 index 000000000000..612d3e6b9b21 --- /dev/null +++ b/runtime/westend/src/weights/auctions.rs @@ -0,0 +1,61 @@ +// Copyright 2017-2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . +//! Autogenerated weights for auctions +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 +//! DATE: 2021-03-14, STEPS: `[50, ]`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("westend-dev"), DB CACHE: 128 + +// Executed Command: +// target/release/polkadot +// benchmark +// --chain=westend-dev +// --steps=50 +// --repeat=20 +// --pallet=auctions +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --header=./file_header.txt +// --output=./runtime/westend/src/weights/ + + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for auctions. +pub struct WeightInfo(PhantomData); +impl auctions::WeightInfo for WeightInfo { + fn new_auction() -> Weight { + (24_105_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + fn bid() -> Weight { + (110_317_000 as Weight) + .saturating_add(T::DbWeight::get().reads(7 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + fn on_initialize() -> Weight { + (602_940_000 as Weight) + .saturating_add(T::DbWeight::get().reads(75 as Weight)) + .saturating_add(T::DbWeight::get().writes(71 as Weight)) + } +} diff --git a/runtime/westend/src/weights/crowdloan.rs b/runtime/westend/src/weights/crowdloan.rs new file mode 100644 index 000000000000..aa405f83ca0f --- /dev/null +++ b/runtime/westend/src/weights/crowdloan.rs @@ -0,0 +1,78 @@ +// Copyright 2017-2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . +//! Autogenerated weights for crowdloan +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 +//! DATE: 2021-03-14, STEPS: `[50, ]`, REPEAT: 20, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("westend-dev"), DB CACHE: 128 + +// Executed Command: +// target/release/polkadot +// benchmark +// --chain=westend-dev +// --steps=50 +// --repeat=20 +// --pallet=crowdloan +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --header=./file_header.txt +// --output=./runtime/westend/src/weights/ + + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for crowdloan. +pub struct WeightInfo(PhantomData); +impl crowdloan::WeightInfo for WeightInfo { + fn create() -> Weight { + (77_552_000 as Weight) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + fn contribute() -> Weight { + (428_901_000 as Weight) + .saturating_add(T::DbWeight::get().reads(6 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + fn withdraw() -> Weight { + (116_845_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + fn dissolve(k: u32, ) -> Weight { + (132_141_000 as Weight) + // Standard Error: 43_000 + .saturating_add((650_000 as Weight).saturating_mul(k as Weight)) + .saturating_add(T::DbWeight::get().reads(3 as Weight)) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(k as Weight))) + } + fn on_initialize(n: u32, ) -> Weight { + (8_340_000 as Weight) + // Standard Error: 18_000 + .saturating_add((106_826_000 as Weight).saturating_mul(n as Weight)) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().reads((4 as Weight).saturating_mul(n as Weight))) + .saturating_add(T::DbWeight::get().writes(3 as Weight)) + .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(n as Weight))) + } +} diff --git a/runtime/westend/src/weights/paras_registrar.rs b/runtime/westend/src/weights/paras_registrar.rs new file mode 100644 index 000000000000..9a9446b77770 --- /dev/null +++ b/runtime/westend/src/weights/paras_registrar.rs @@ -0,0 +1,61 @@ +// Copyright 2017-2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . +//! Autogenerated weights for paras_registrar +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 +//! DATE: 2021-02-21, STEPS: [50, ], REPEAT: 20, LOW RANGE: [], HIGH RANGE: [] +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("westend-dev"), DB CACHE: 128 + +// Executed Command: +// target/release/polkadot +// benchmark +// --chain=westend-dev +// --steps=50 +// --repeat=20 +// --pallet=paras_registrar +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --header=./file_header.txt +// --output=./runtime/westend/src/weights/ + + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for paras_registrar. +pub struct WeightInfo(PhantomData); +impl paras_registrar::WeightInfo for WeightInfo { + fn register() -> Weight { + (8_442_552_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + fn deregister() -> Weight { + (89_379_000 as Weight) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } + fn swap() -> Weight { + (61_913_000 as Weight) + .saturating_add(T::DbWeight::get().reads(5 as Weight)) + .saturating_add(T::DbWeight::get().writes(4 as Weight)) + } +} diff --git a/runtime/westend/src/weights/slots.rs b/runtime/westend/src/weights/slots.rs new file mode 100644 index 000000000000..53454ab80a12 --- /dev/null +++ b/runtime/westend/src/weights/slots.rs @@ -0,0 +1,64 @@ +// Copyright 2017-2020 Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot 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. + +// Polkadot 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 Polkadot. If not, see . +//! Autogenerated weights for slots +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 3.0.0 +//! DATE: 2021-03-01, STEPS: [50, ], REPEAT: 20, LOW RANGE: [], HIGH RANGE: [] +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("westend-dev"), DB CACHE: 128 + +// Executed Command: +// target/release/polkadot +// benchmark +// --chain=westend-dev +// --steps=50 +// --repeat=20 +// --pallet=slots +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --header=./file_header.txt +// --output=./runtime/westend/src/weights/ + + +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::Weight}; +use sp_std::marker::PhantomData; + +/// Weight functions for slots. +pub struct WeightInfo(PhantomData); +impl slots::WeightInfo for WeightInfo { + fn force_lease() -> Weight { + (46_565_000 as Weight) + .saturating_add(T::DbWeight::get().reads(2 as Weight)) + .saturating_add(T::DbWeight::get().writes(2 as Weight)) + } + fn manage_lease_period_start(c: u32, t: u32, ) -> Weight { + (0 as Weight) + // Standard Error: 18_000 + .saturating_add((17_965_000 as Weight).saturating_mul(c as Weight)) + // Standard Error: 18_000 + .saturating_add((34_217_000 as Weight).saturating_mul(t as Weight)) + .saturating_add(T::DbWeight::get().reads(4 as Weight)) + .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(c as Weight))) + .saturating_add(T::DbWeight::get().reads((2 as Weight).saturating_mul(t as Weight))) + .saturating_add(T::DbWeight::get().writes(1 as Weight)) + .saturating_add(T::DbWeight::get().writes((1 as Weight).saturating_mul(c as Weight))) + .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(t as Weight))) + } +}