diff --git a/Cargo.lock b/Cargo.lock index 87a2dad75494..dcaacf7d8dd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13248,6 +13248,7 @@ dependencies = [ "pallet-vesting", "pallet-xcm-benchmarks", "parity-scale-codec", + "polkadot-parachain-primitives", "polkadot-primitives", "polkadot-primitives-test-helpers", "polkadot-runtime-parachains", diff --git a/cumulus/zombienet/tests/0004-runtime_upgrade.toml b/cumulus/zombienet/tests/0004-runtime_upgrade.toml index fbf0dfc69163..ca7d557c125c 100644 --- a/cumulus/zombienet/tests/0004-runtime_upgrade.toml +++ b/cumulus/zombienet/tests/0004-runtime_upgrade.toml @@ -3,7 +3,7 @@ default_image = "{{RELAY_IMAGE}}" default_command = "polkadot" default_args = [ "-lparachain=debug" ] -chain = "rococo-local" +chain = "westend-local" [[relaychain.nodes]] name = "alice" diff --git a/cumulus/zombienet/tests/0004-runtime_upgrade.zndsl b/cumulus/zombienet/tests/0004-runtime_upgrade.zndsl index cdafc48fce9e..cf5b637f3f10 100644 --- a/cumulus/zombienet/tests/0004-runtime_upgrade.zndsl +++ b/cumulus/zombienet/tests/0004-runtime_upgrade.zndsl @@ -4,6 +4,7 @@ Creds: config alice: parachain 2000 is registered within 225 seconds charlie: reports block height is at least 5 within 250 seconds + charlie: parachain 2000 perform upgrade with /tmp/wasm_binary_spec_version_incremented.rs.compact.compressed.wasm within 200 seconds dave: reports block height is at least 20 within 250 seconds dave: js-script ./runtime_upgrade.js within 200 seconds diff --git a/polkadot/runtime/common/Cargo.toml b/polkadot/runtime/common/Cargo.toml index f05963091dd6..a8058d8acd32 100644 --- a/polkadot/runtime/common/Cargo.toml +++ b/polkadot/runtime/common/Cargo.toml @@ -46,6 +46,7 @@ pallet-transaction-payment = { path = "../../../substrate/frame/transaction-paym pallet-treasury = { path = "../../../substrate/frame/treasury", default-features = false } pallet-asset-rate = { path = "../../../substrate/frame/asset-rate", default-features = false, optional = true } pallet-election-provider-multi-phase = { path = "../../../substrate/frame/election-provider-multi-phase", default-features = false } +polkadot-parachain-primitives = { path = "../../parachain", default-features = false } frame-election-provider-support = { path = "../../../substrate/frame/election-provider-support", default-features = false } frame-benchmarking = { path = "../../../substrate/frame/benchmarking", default-features = false, optional = true } @@ -57,7 +58,7 @@ runtime-parachains = { package = "polkadot-runtime-parachains", path = "../parac slot-range-helper = { path = "slot_range_helper", default-features = false } xcm = { package = "staging-xcm", path = "../../xcm", default-features = false } -xcm-executor = { package = "staging-xcm-executor", path = "../../xcm/xcm-executor", default-features = false, optional = true } +xcm-executor = { package = "staging-xcm-executor", path = "../../xcm/xcm-executor", default-features = false } pallet-xcm-benchmarks = { path = "../../xcm/pallet-xcm-benchmarks", default-features = false, optional = true } xcm-builder = { package = "staging-xcm-builder", path = "../../xcm/xcm-builder", default-features = false } @@ -101,6 +102,7 @@ std = [ "pallet-vesting/std", "pallet-xcm-benchmarks/std", "parity-scale-codec/std", + "polkadot-parachain-primitives/std", "primitives/std", "runtime-parachains/std", "rustc-hex/std", @@ -138,6 +140,7 @@ runtime-benchmarks = [ "pallet-treasury/runtime-benchmarks", "pallet-vesting/runtime-benchmarks", "pallet-xcm-benchmarks/runtime-benchmarks", + "polkadot-parachain-primitives/runtime-benchmarks", "primitives/runtime-benchmarks", "runtime-parachains/runtime-benchmarks", "sp-runtime/runtime-benchmarks", diff --git a/polkadot/runtime/common/src/assigned_slots/mod.rs b/polkadot/runtime/common/src/assigned_slots/mod.rs index de8c00df9ddf..2e0acf2b549b 100644 --- a/polkadot/runtime/common/src/assigned_slots/mod.rs +++ b/polkadot/runtime/common/src/assigned_slots/mod.rs @@ -742,6 +742,8 @@ mod tests { type UnsignedPriority = ParasUnsignedPriority; type QueueFootprinter = (); type NextSessionRotation = crate::mock::TestNextSessionRotation; + type PreCodeUpgrade = (); + type OnCodeUpgraded = (); type OnNewHead = (); type AssignCoretime = (); } diff --git a/polkadot/runtime/common/src/integration_tests.rs b/polkadot/runtime/common/src/integration_tests.rs index b536b80e2452..e534548dfbc8 100644 --- a/polkadot/runtime/common/src/integration_tests.rs +++ b/polkadot/runtime/common/src/integration_tests.rs @@ -32,6 +32,7 @@ use frame_support::{ }; use frame_support_test::TestRandomness; use frame_system::EnsureRoot; +use pallet_balances::Error as BalancesError; use pallet_identity::{self, legacy::IdentityInfo}; use parity_scale_codec::Encode; use primitives::{ @@ -45,11 +46,12 @@ use sp_io::TestExternalities; use sp_keyring::Sr25519Keyring; use sp_keystore::{testing::MemoryKeystore, KeystoreExt}; use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup, One, Verify}, + traits::{AccountIdConversion, BlakeTwo256, IdentityLookup, One, Verify}, transaction_validity::TransactionPriority, AccountId32, BuildStorage, MultiSignature, }; use sp_std::sync::Arc; +use xcm::opaque::lts::NetworkId; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlockU32; @@ -213,6 +215,8 @@ impl paras::Config for Test { type UnsignedPriority = ParasUnsignedPriority; type QueueFootprinter = (); type NextSessionRotation = crate::mock::TestNextSessionRotation; + type PreCodeUpgrade = Registrar; + type OnCodeUpgraded = Registrar; type OnNewHead = (); type AssignCoretime = (); } @@ -220,6 +224,8 @@ impl paras::Config for Test { parameter_types! { pub const ParaDeposit: Balance = 500; pub const DataDepositPerByte: Balance = 1; + pub const UpgradeFee: Balance = 2; + pub const RelayNetwork: NetworkId = NetworkId::Kusama; } impl paras_registrar::Config for Test { @@ -229,6 +235,7 @@ impl paras_registrar::Config for Test { type DataDepositPerByte = DataDepositPerByte; type Currency = Balances; type RuntimeOrigin = RuntimeOrigin; + type UpgradeFee = UpgradeFee; type WeightInfo = crate::paras_registrar::TestWeightInfo; } @@ -382,7 +389,9 @@ fn run_to_block(n: u32) { AllPalletsWithSystem::on_finalize(block_number); System::set_block_number(block_number + 1); maybe_new_session(block_number + 1); + Paras::initializer_initialize(block_number + 1); AllPalletsWithSystem::on_initialize(block_number + 1); + Paras::initializer_finalize(block_number + 1); } } @@ -399,6 +408,12 @@ fn contains_event(event: RuntimeEvent) -> bool { System::events().iter().any(|x| x.event == event) } +/// Helper function for getting a custom size validation code. +fn validation_code(size: usize) -> ValidationCode { + let validation_code = vec![0u8; size]; + validation_code.into() +} + // Runs an end to end test of the auction, crowdloan, slots, and onboarding process over varying // lease period offsets. #[test] @@ -426,7 +441,7 @@ fn basic_end_to_end_works() { genesis_head.clone(), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); assert_ok!(Registrar::reserve(signed(2))); assert_ok!(Registrar::register( signed(2), @@ -588,6 +603,553 @@ fn basic_end_to_end_works() { } } +#[test] +fn para_upgrade_initiated_by_manager_works() { + new_test_ext().execute_with(|| { + assert!(System::block_number().is_one()); /* So events are emitted */ + let para_id = LOWEST_PUBLIC_ID; + const START_SESSION_INDEX: SessionIndex = 1; + run_to_session(START_SESSION_INDEX); + + // User 1 will own a parachain + let free_balance = 1_000_000_000; + Balances::make_free_balance_be(&account_id(1), free_balance); + // Register an on demand parachain + let mut code_size = 1024 * 1024; + let genesis_head = Registrar::worst_head_data(); + let head_size = genesis_head.0.len(); + let code_0 = validation_code(code_size); + + assert_ok!(Registrar::reserve(signed(1))); + assert_ok!(Registrar::register( + signed(1), + ParaId::from(para_id), + genesis_head.clone(), + code_0.clone(), + )); + conclude_pvf_checking::(&code_0, VALIDATORS, START_SESSION_INDEX, true); + + // The para should be onboarding. + assert_eq!(Paras::lifecycle(ParaId::from(para_id)), Some(ParaLifecycle::Onboarding)); + // After two sessions the parachain will be succesfully registered as an on-demand. + run_to_session(START_SESSION_INDEX + 2); + assert!(Registrar::is_parathread(para_id)); + // The deposit should be appropriately taken. + let total_bytes_stored = code_size as u32 + head_size as u32; + assert_eq!( + Balances::reserved_balance(&account_id(1)), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + + // CASE 1: Attempting to schedule a parachain upgrade without setting the billing account + // beforehand will result in failure. + assert_noop!( + Registrar::schedule_code_upgrade(signed(1), ParaId::from(para_id), code_0,), + paras_registrar::Error::::CannotUpgrade + ); + + // Set the billing account to be able to schedule code upgrades. + assert_ok!(Registrar::set_parachain_billing_account_to_self( + signed(1), + ParaId::from(para_id), + )); + + // CASE 2: Schedule a para upgrade to set the validation code to a new one which is twice + // the size. + code_size *= 2; + let code_1 = validation_code(code_size); + assert_ok!(Registrar::schedule_code_upgrade( + signed(1), + ParaId::from(para_id), + code_1.clone(), + )); + + // The reserved deposit should cover for the size difference of the new validation code. + let total_bytes_stored = code_size as u32 + head_size as u32; + assert_eq!( + Balances::reserved_balance(&account_id(1)), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + // An additional upgrade fee should also be deducted from the caller's balance. + assert_eq!(Balances::total_balance(&account_id(1)), free_balance - UpgradeFee::get()); + + conclude_pvf_checking::(&code_1, VALIDATORS, START_SESSION_INDEX + 2, true); + // After two more sessions the parachain can be upgraded. + run_to_session(START_SESSION_INDEX + 4); + // Force a new head to enact the code upgrade. + assert_ok!(Paras::force_note_new_head( + RuntimeOrigin::root(), + para_id, + genesis_head.clone() + )); + assert_eq!(Paras::current_code(¶_id), Some(code_1.clone())); + + // CASE 3: After successfully upgrading the validation code to twice the size of the + // previous one, we will now proceed to upgrade the validation code to a smaller size. It is + // expected that the parachain manager will receive a refund upon the successful completion + // of the upgrade. + + code_size /= 2; + let code_2 = validation_code(code_size); + assert_ok!(Registrar::schedule_code_upgrade( + signed(1), + ParaId::from(para_id), + code_2.clone(), + )); + conclude_pvf_checking::(&code_2, VALIDATORS, START_SESSION_INDEX + 4, true); + + // After two more sessions the parachain can be upgraded. + run_to_session(START_SESSION_INDEX + 6); + // Force a new head to enact the code upgrade. + assert_ok!(Paras::force_note_new_head( + RuntimeOrigin::root(), + para_id, + genesis_head.clone() + )); + assert_eq!(Paras::current_code(¶_id), Some(code_2.clone())); + + let updated_total_bytes_stored = code_size as u32 + head_size as u32; + let expected_rebate = + (total_bytes_stored - updated_total_bytes_stored) * DataDepositPerByte::get(); + + assert!(contains_event( + paras_registrar::Event::::Refunded { + para_id, + who: account_id(1), + amount: expected_rebate + } + .into() + ),); + // The reserved deposit should cover only the size of the new validation code. + assert_eq!( + Balances::reserved_balance(&account_id(1)), + ParaDeposit::get() + (updated_total_bytes_stored * DataDepositPerByte::get()) + ); + // An additional upgrade fee should also be deducted from the caller's balance. + assert_eq!(Balances::total_balance(&account_id(1)), free_balance - (2 * UpgradeFee::get())); + + // CASE 4: Para manager won't get refunded if the code upgrade fails + let code_3 = validation_code(42); + assert_ok!(Registrar::schedule_code_upgrade( + signed(1), + ParaId::from(para_id), + code_3.clone(), + )); + // Reject the new validation code. + conclude_pvf_checking::(&code_3, VALIDATORS, START_SESSION_INDEX + 6, false); + + // After two more sessions the parachain can be upgraded. + run_to_session(START_SESSION_INDEX + 8); + // Force a new head to enact the code upgrade. + assert_ok!(Paras::force_note_new_head(RuntimeOrigin::root(), para_id, Default::default())); + // The upgrade failed and the para manager shouldn't get a refund. + assert_eq!(Paras::current_code(¶_id), Some(code_2.clone())); + assert_eq!( + Balances::reserved_balance(&account_id(1)), + ParaDeposit::get() + (updated_total_bytes_stored * DataDepositPerByte::get()) + ); + // Even though the upgrade failed the upgrade fee is deducted from the caller's balance. + assert_eq!(Balances::total_balance(&account_id(1)), free_balance - (3 * UpgradeFee::get())); + }); +} + +#[test] +fn root_upgrading_parachain_works() { + new_test_ext().execute_with(|| { + assert!(System::block_number().is_one()); /* So events are emitted */ + let para_id = LOWEST_PUBLIC_ID; + const START_SESSION_INDEX: SessionIndex = 1; + run_to_session(START_SESSION_INDEX); + + // User 1 will own a parachain + Balances::make_free_balance_be(&account_id(1), 1_000_000_000); + // Register an on demand parachain + let code_size = 1024 * 1024; + let genesis_head = Registrar::worst_head_data(); + let head_size = genesis_head.0.len(); + let code_0 = validation_code(code_size); + + assert_ok!(Registrar::reserve(signed(1))); + assert_ok!(Registrar::register( + signed(1), + ParaId::from(para_id), + genesis_head.clone(), + code_0.clone(), + )); + conclude_pvf_checking::(&code_0, VALIDATORS, START_SESSION_INDEX, true); + + // The para should be onboarding. + assert_eq!(Paras::lifecycle(ParaId::from(para_id)), Some(ParaLifecycle::Onboarding)); + // After two sessions the parachain will be succesfully registered as an on-demand. + run_to_session(START_SESSION_INDEX + 2); + assert!(Registrar::is_parathread(para_id)); + // The deposit should be appropriately taken. + let total_bytes_stored = code_size as u32 + head_size as u32; + assert_eq!( + Balances::reserved_balance(&account_id(1)), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + + // Set the billing account to be able to schedule code upgrades. + assert_ok!(Registrar::set_parachain_billing_account_to_self( + signed(1), + ParaId::from(para_id), + )); + + // CASE 1: Root schedules a para upgrade to set the validation code to a new one which is + // twice the size. + + let code_1 = validation_code(code_size * 2); + assert_ok!(Registrar::schedule_code_upgrade( + RuntimeOrigin::root(), + ParaId::from(para_id), + code_1.clone(), + )); + conclude_pvf_checking::(&code_1, VALIDATORS, START_SESSION_INDEX + 2, true); + + // After two more sessions the parachain can be upgraded. + run_to_session(START_SESSION_INDEX + 4); + // Force a new head to enact the code upgrade. + assert_ok!(Paras::force_note_new_head( + RuntimeOrigin::root(), + para_id, + genesis_head.clone() + )); + assert_eq!(Paras::current_code(¶_id), Some(code_1.clone())); + + // The reserved deposit should remain the same since the upgrade was performed by root. + let total_bytes_stored = code_size as u32 + head_size as u32; + assert_eq!( + Balances::reserved_balance(&account_id(1)), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + + // CASE 2: Root schedules a para upgrade to set the validation code to a new one which has + // half the size of the initially registered code. The billing acount should get a refund + // for the deposit they are no longer required to hold. + + let code_2 = validation_code(code_size / 2); + assert_ok!(Registrar::schedule_code_upgrade( + RuntimeOrigin::root(), + ParaId::from(para_id), + code_2.clone(), + )); + conclude_pvf_checking::(&code_2, VALIDATORS, START_SESSION_INDEX + 4, true); + + // After two more sessions the parachain can be upgraded. + run_to_session(START_SESSION_INDEX + 6); + // Force a new head to enact the code upgrade. + assert_ok!(Paras::force_note_new_head( + RuntimeOrigin::root(), + para_id, + genesis_head.clone() + )); + assert_eq!(Paras::current_code(¶_id), Some(code_2.clone())); + + let total_bytes_stored = (code_size / 2) as u32 + head_size as u32; + assert_eq!( + Balances::reserved_balance(&account_id(1)), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + }); +} + +#[test] +fn lease_holding_parachains_have_no_upgrade_costs() { + new_test_ext().execute_with(|| { + assert!(System::block_number().is_one()); /* So events are emitted */ + let para_id = LOWEST_PUBLIC_ID; + const START_SESSION_INDEX: SessionIndex = 1; + run_to_session(START_SESSION_INDEX); + + // User 1 will own a parachain + let free_balance = 1_000_000_000; + Balances::make_free_balance_be(&account_id(1), free_balance); + // Register an on demand parachain + let mut code_size = 1024 * 1024; + let genesis_head = Registrar::worst_head_data(); + let head_size = genesis_head.0.len(); + let code_0 = validation_code(code_size); + + assert_ok!(Registrar::reserve(signed(1))); + assert_ok!(Registrar::register( + signed(1), + ParaId::from(para_id), + genesis_head.clone(), + code_0.clone(), + )); + conclude_pvf_checking::(&code_0, VALIDATORS, START_SESSION_INDEX, true); + + // The para should be onboarding. + assert_eq!(Paras::lifecycle(ParaId::from(para_id)), Some(ParaLifecycle::Onboarding)); + // After two sessions the parachain will be succesfully registered as an on-demand. + run_to_session(START_SESSION_INDEX + 2); + assert!(Registrar::is_parathread(para_id)); + + // The deposit should be appropriately taken. + let total_bytes_stored = code_size as u32 + head_size as u32; + assert_eq!( + Balances::reserved_balance(&account_id(1)), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + + // Make the para a lease holding parachain. + assert_ok!(Registrar::make_parachain(para_id)); + run_to_session(START_SESSION_INDEX + 4); + assert!(Registrar::is_parachain(para_id)); + + // The lease holding parachain should be able to schedule a code upgrade without any + // additional deposit or fees necessary. + + code_size *= 2; + let code_1 = validation_code(code_size); + assert_ok!(Registrar::schedule_code_upgrade( + signed(1), + ParaId::from(para_id), + code_1.clone(), + )); + conclude_pvf_checking::(&code_1, VALIDATORS, START_SESSION_INDEX + 4, true); + + // After two more sessions the parachain can be upgraded. + run_to_session(START_SESSION_INDEX + 6); + // Force a new head to enact the code upgrade. + assert_ok!(Paras::force_note_new_head( + RuntimeOrigin::root(), + para_id, + genesis_head.clone() + )); + assert_eq!(Paras::current_code(¶_id), Some(code_1.clone())); + + // Should remain unchanged: + assert_eq!( + Balances::reserved_balance(&account_id(1)), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + assert_eq!(Balances::total_balance(&account_id(1)), free_balance); + }); +} + +#[test] +fn setting_parachain_billing_account_to_self_works() { + new_test_ext().execute_with(|| { + assert!(System::block_number().is_one()); /* So events are emitted */ + let para_id = LOWEST_PUBLIC_ID; + const START_SESSION_INDEX: SessionIndex = 1; + run_to_session(START_SESSION_INDEX); + + // User 1 will own a parachain + let free_balance = 1_000_000_000; + Balances::make_free_balance_be(&account_id(1), free_balance); + // Register an on demand parachain + let mut code_size = 1024 * 1024; + let genesis_head = Registrar::worst_head_data(); + let head_size = genesis_head.0.len(); + let code_0 = validation_code(code_size); + + assert_ok!(Registrar::reserve(signed(1))); + assert_ok!(Registrar::register( + signed(1), + ParaId::from(para_id), + genesis_head.clone(), + code_0.clone(), + )); + conclude_pvf_checking::(&code_0, VALIDATORS, START_SESSION_INDEX, true); + + // The para should be onboarding. + assert_eq!(Paras::lifecycle(ParaId::from(para_id)), Some(ParaLifecycle::Onboarding)); + // After two sessions the parachain will be succesfully registered as an on-demand. + run_to_session(START_SESSION_INDEX + 2); + assert!(Registrar::is_parathread(para_id)); + // The deposit should be appropriately taken. + let total_bytes_stored = code_size as u32 + head_size as u32; + assert_eq!( + Balances::reserved_balance(&account_id(1)), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + + // CASE 1: Parachain origin attempting to assign the billing account to it itself will + // fail if the balance of the sovereign account is insufficient to cover the required + // deposit amount + + let sovereign_account = para_id.into_account_truncating(); + let para_origin: runtime_parachains::Origin = u32::from(para_id).into(); + + assert_noop!( + Registrar::set_parachain_billing_account_to_self( + para_origin.clone().into(), + ParaId::from(para_id) + ), + BalancesError::::InsufficientBalance + ); + + // CASE 2: The happy path.. The new billing account is sufficiently funded, so the + // reserve will be successful, and the old account will be refunded. + Balances::make_free_balance_be(&sovereign_account, free_balance); + + assert_ok!(Registrar::set_parachain_billing_account_to_self( + para_origin.into(), + ParaId::from(para_id), + )); + assert_eq!( + last_event(), + paras_registrar::Event::::BillingAccountSet { + para_id, + who: sovereign_account.clone() + } + .into(), + ); + + assert_eq!( + Balances::reserved_balance(&sovereign_account), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + assert_eq!(Balances::free_balance(&account_id(1)), free_balance); + + // Now, if we attempt to schedule a code upgrade, all the costs will be covered by the new + // billing account. + + code_size *= 2; + let code_1 = validation_code(code_size); + assert_ok!(Registrar::schedule_code_upgrade( + signed(1), + ParaId::from(para_id), + code_1.clone(), + )); + conclude_pvf_checking::(&code_1, VALIDATORS, START_SESSION_INDEX + 2, true); + + // After two more sessions the parachain can be upgraded. + run_to_session(START_SESSION_INDEX + 4); + // Force a new head to enact the code upgrade. + assert_ok!(Paras::force_note_new_head( + RuntimeOrigin::root(), + para_id, + genesis_head.clone() + )); + assert_eq!(Paras::current_code(¶_id), Some(code_1.clone())); + + // The new code upgrade billing account will have its deposit appropriately adjusted given + // the increase in the new code. + let total_bytes_stored = code_size as u32 + head_size as u32; + assert_eq!( + Balances::reserved_balance(&sovereign_account), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + // The new billing account will also be charged with the upgrade fees. + assert_eq!(Balances::total_balance(&sovereign_account), free_balance - UpgradeFee::get()); + + // The account of the initial billing account should however remain unchanged. + assert_eq!(Balances::free_balance(&account_id(1)), free_balance); + }); +} + +#[test] +fn force_set_parachain_billing_account_works() { + new_test_ext().execute_with(|| { + assert!(System::block_number().is_one()); /* So events are emitted */ + let para_id = LOWEST_PUBLIC_ID; + const START_SESSION_INDEX: SessionIndex = 1; + run_to_session(START_SESSION_INDEX); + + // User 1 will own a parachain + let free_balance = 1_000_000_000; + Balances::make_free_balance_be(&account_id(1), free_balance); + // Register an on demand parachain + let mut code_size = 1024 * 1024; + let genesis_head = Registrar::worst_head_data(); + let head_size = genesis_head.0.len(); + let code_0 = validation_code(code_size); + + assert_ok!(Registrar::reserve(signed(1))); + assert_ok!(Registrar::register( + signed(1), + ParaId::from(para_id), + genesis_head.clone(), + code_0.clone(), + )); + conclude_pvf_checking::(&code_0, VALIDATORS, START_SESSION_INDEX, true); + + // The para should be onboarding. + assert_eq!(Paras::lifecycle(ParaId::from(para_id)), Some(ParaLifecycle::Onboarding)); + // After two sessions the parachain will be succesfully registered as an on-demand. + run_to_session(START_SESSION_INDEX + 2); + assert!(Registrar::is_parathread(para_id)); + // The deposit should be appropriately taken. + let total_bytes_stored = code_size as u32 + head_size as u32; + assert_eq!( + Balances::reserved_balance(&account_id(1)), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + + // CASE 1: Attempting to set the billing account to an account with an insufficient balance + // to cover the required deposit should result in a failure. + + assert_noop!( + Registrar::force_set_parachain_billing_account( + RuntimeOrigin::root(), + ParaId::from(para_id), + account_id(2), + ), + BalancesError::::InsufficientBalance + ); + + // CASE 2: The new billing account is sufficiently funded, so the reserve will be + // successful, and the old account will be refunded. + Balances::make_free_balance_be(&account_id(2), free_balance); + + assert_ok!(Registrar::force_set_parachain_billing_account( + RuntimeOrigin::root(), + ParaId::from(para_id), + account_id(2) + )); + assert_eq!( + last_event(), + paras_registrar::Event::::BillingAccountSet { para_id, who: account_id(2) } + .into(), + ); + + assert_eq!( + Balances::reserved_balance(&account_id(2)), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + assert_eq!(Balances::free_balance(&account_id(1)), free_balance); + + // Now, if we attempt to schedule a code upgrade, all the costs will be covered by the new + // billing account. + + code_size *= 2; + let code_1 = validation_code(code_size); + assert_ok!(Registrar::schedule_code_upgrade( + signed(1), + ParaId::from(para_id), + code_1.clone(), + )); + conclude_pvf_checking::(&code_1, VALIDATORS, START_SESSION_INDEX + 2, true); + + // After two more sessions the parachain can be upgraded. + run_to_session(START_SESSION_INDEX + 4); + // Force a new head to enact the code upgrade. + assert_ok!(Paras::force_note_new_head( + RuntimeOrigin::root(), + para_id, + genesis_head.clone() + )); + assert_eq!(Paras::current_code(¶_id), Some(code_1.clone())); + + // The new code upgrade billing account will have its deposit appropriately adjusted given + // the increase in the new code. + let total_bytes_stored = code_size as u32 + head_size as u32; + assert_eq!( + Balances::reserved_balance(&account_id(2)), + ParaDeposit::get() + (total_bytes_stored * DataDepositPerByte::get()) + ); + // The new billing account will also be charged with the upgrade fees. + assert_eq!(Balances::total_balance(&account_id(2)), free_balance - UpgradeFee::get()); + + // The account of the initial billing account should however remain unchanged. + assert_eq!(Balances::free_balance(&account_id(1)), free_balance); + }); +} + #[test] fn basic_errors_fail() { new_test_ext().execute_with(|| { @@ -663,7 +1225,7 @@ fn competing_slots() { )); } // The code undergoing the prechecking is the same for all paras. - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); // Start a new auction in the future let duration = 149u32; @@ -765,7 +1327,7 @@ fn competing_bids() { )); } // The code undergoing the prechecking is the same for all paras. - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); // Finish registration of paras. run_to_session(START_SESSION_INDEX + 2); @@ -872,7 +1434,7 @@ fn basic_swap_works() { test_genesis_head(10), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); let validation_code = test_validation_code(20); assert_ok!(Registrar::reserve(signed(2))); @@ -882,7 +1444,7 @@ fn basic_swap_works() { test_genesis_head(20), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); // Paras should be onboarding assert_eq!(Paras::lifecycle(ParaId::from(2000)), Some(ParaLifecycle::Onboarding)); @@ -1034,7 +1596,7 @@ fn parachain_swap_works() { test_genesis_head(10), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); let validation_code = test_validation_code(20); assert_ok!(Registrar::reserve(signed(2))); @@ -1044,7 +1606,7 @@ fn parachain_swap_works() { test_genesis_head(20), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); // Paras should be onboarding assert_eq!(Paras::lifecycle(ParaId::from(2000)), Some(ParaLifecycle::Onboarding)); @@ -1212,7 +1774,7 @@ fn crowdloan_ending_period_bid() { test_genesis_head(10), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); let validation_code = test_validation_code(20); assert_ok!(Registrar::reserve(signed(2))); @@ -1222,7 +1784,7 @@ fn crowdloan_ending_period_bid() { test_genesis_head(20), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); // Paras should be onboarding assert_eq!(Paras::lifecycle(ParaId::from(2000)), Some(ParaLifecycle::Onboarding)); @@ -1344,7 +1906,7 @@ fn auction_bid_requires_registered_para() { test_genesis_head(10), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); // Still can't bid until it is fully onboarded assert_noop!( @@ -1410,7 +1972,7 @@ fn gap_bids_work() { test_genesis_head(10), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); // Onboarded on Session 2 run_to_session(START_SESSION_INDEX + 2); @@ -1580,7 +2142,7 @@ fn cant_bid_on_existing_lease_periods() { test_genesis_head(10), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); // Start a new auction in the future let starting_block = System::block_number(); diff --git a/polkadot/runtime/common/src/mock.rs b/polkadot/runtime/common/src/mock.rs index c9e3a8c39f12..0462bd334f18 100644 --- a/polkadot/runtime/common/src/mock.rs +++ b/polkadot/runtime/common/src/mock.rs @@ -247,12 +247,13 @@ pub fn conclude_pvf_checking( validation_code: &ValidationCode, validators: &[Sr25519Keyring], session_index: SessionIndex, + accept: bool, ) { let num_required = primitives::supermajority_threshold(validators.len()); validators.iter().enumerate().take(num_required).for_each(|(idx, key)| { let validator_index = idx as u32; let statement = PvfCheckStatement { - accept: true, + accept, subject: validation_code.hash(), session_index, validator_index: validator_index.into(), diff --git a/polkadot/runtime/common/src/paras_registrar/migration.rs b/polkadot/runtime/common/src/paras_registrar/migration.rs index f977674a1e4e..efc7825b18f7 100644 --- a/polkadot/runtime/common/src/paras_registrar/migration.rs +++ b/polkadot/runtime/common/src/paras_registrar/migration.rs @@ -15,55 +15,132 @@ // along with Polkadot. If not, see . use super::*; -use frame_support::traits::{Contains, OnRuntimeUpgrade}; +use frame_support::{ + traits::{Contains, OnRuntimeUpgrade}, + Twox64Concat, +}; #[derive(Encode, Decode)] pub struct ParaInfoV1 { manager: Account, deposit: Balance, - locked: bool, + locked: Option, } -pub struct VersionUncheckedMigrateToV1( - sp_std::marker::PhantomData<(T, UnlockParaIds)>, -); -impl> OnRuntimeUpgrade - for VersionUncheckedMigrateToV1 -{ - fn on_runtime_upgrade() -> Weight { - let mut count = 0u64; - Paras::::translate::>, _>(|key, v1| { - count.saturating_inc(); - Some(ParaInfo { - manager: v1.manager, - deposit: v1.deposit, - locked: if UnlockParaIds::contains(&key) { None } else { Some(v1.locked) }, - }) - }); +pub mod v1 { + use super::*; - log::info!(target: "runtime::registrar", "Upgraded {} storages to version 1", count); - T::DbWeight::get().reads_writes(count, count) - } + #[frame_support::storage_alias] + pub type Paras = StorageMap< + Pallet, + Twox64Concat, + ParaId, + ParaInfoV1<::AccountId, BalanceOf>, + >; - #[cfg(feature = "try-runtime")] - fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { - Ok((Paras::::iter_keys().count() as u32).encode()) + #[derive(Encode, Decode)] + pub struct ParaInfoOld { + manager: Account, + deposit: Balance, + locked: bool, } - #[cfg(feature = "try-runtime")] - fn post_upgrade(state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { - let old_count = u32::decode(&mut &state[..]).expect("Known good"); - let new_count = Paras::::iter_values().count() as u32; + pub struct VersionUncheckedMigrateToV1( + sp_std::marker::PhantomData<(T, UnlockParaIds)>, + ); + impl> OnRuntimeUpgrade + for VersionUncheckedMigrateToV1 + { + fn on_runtime_upgrade() -> Weight { + let mut count = 0u64; + Paras::::translate::>, _>(|key, v1| { + count.saturating_inc(); + Some(ParaInfoV1 { + manager: v1.manager, + deposit: v1.deposit, + locked: if UnlockParaIds::contains(&key) { None } else { Some(v1.locked) }, + }) + }); + + log::info!(target: "runtime::registrar", "Upgraded {} storages to version 1", count); + T::DbWeight::get().reads_writes(count, count) + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + Ok((Paras::::iter_keys().count() as u32).encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { + let old_count = u32::decode(&mut &state[..]).expect("Known good"); + let new_count = Paras::::iter_values().count() as u32; - ensure!(old_count == new_count, "Paras count should not change"); - Ok(()) + ensure!(old_count == new_count, "Paras count should not change"); + Ok(()) + } } + + pub type MigrateToV1 = frame_support::migrations::VersionedMigration< + 0, + 1, + VersionUncheckedMigrateToV1, + super::Pallet, + ::DbWeight, + >; } -pub type MigrateToV1 = frame_support::migrations::VersionedMigration< - 0, - 1, - VersionUncheckedMigrateToV1, - super::Pallet, - ::DbWeight, ->; +pub mod v2 { + use super::*; + + pub struct VersionUncheckedMigrateToV2(sp_std::marker::PhantomData); + impl OnRuntimeUpgrade for VersionUncheckedMigrateToV2 { + fn on_runtime_upgrade() -> Weight { + let mut count = 0u64; + Paras::::translate::>, _>(|_key, v1| { + count.saturating_inc(); + Some(ParaInfo { + manager: v1.manager, + deposit: v1.deposit, + locked: v1.locked, + billing_account: None, + pending_deposit_refund: None, + }) + }); + + log::info!(target: "runtime::registrar", "Upgraded {} storages to version 2", count); + T::DbWeight::get().reads_writes(count, count) + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result, sp_runtime::TryRuntimeError> { + Ok((Paras::::iter_keys().count() as u32).encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { + let old_count = u32::decode(&mut &state[..]).expect("Known good"); + let new_count = Paras::::iter_values().count() as u32; + + ensure!(old_count == new_count, "Paras count should not change"); + + Paras::::iter().try_for_each(|(_para_id, info)| -> Result<(), _> { + ensure!( + info.billing_account.is_none(), + "The billing account must be set to the None" + ); + ensure!(info.pending_deposit_refund.is_none(), "There should be no pending refund"); + + Ok(()) + }) + } + } + + pub type MigrateToV2 = frame_support::migrations::VersionedMigration< + 1, + 2, + VersionUncheckedMigrateToV2, + super::Pallet, + ::DbWeight, + >; +} diff --git a/polkadot/runtime/common/src/paras_registrar/mod.rs b/polkadot/runtime/common/src/paras_registrar/mod.rs index 448490b34a7f..6716cb79bdb4 100644 --- a/polkadot/runtime/common/src/paras_registrar/mod.rs +++ b/polkadot/runtime/common/src/paras_registrar/mod.rs @@ -23,13 +23,15 @@ use frame_support::{ dispatch::DispatchResult, ensure, pallet_prelude::Weight, - traits::{Currency, Get, ReservableCurrency}, + traits::{Currency, ExistenceRequirement, Get, ReservableCurrency, WithdrawReasons}, + transactional, }; use frame_system::{self, ensure_root, ensure_signed}; +use polkadot_parachain_primitives::primitives::IsSystem; use primitives::{HeadData, Id as ParaId, ValidationCode, LOWEST_PUBLIC_ID}; use runtime_parachains::{ configuration, ensure_parachain, - paras::{self, ParaGenesisArgs, SetGoAhead}, + paras::{self, OnCodeUpgraded, ParaGenesisArgs, PreCodeUpgrade, SetGoAhead}, Origin, ParaLifecycle, }; use sp_std::{prelude::*, result}; @@ -37,22 +39,37 @@ use sp_std::{prelude::*, result}; use crate::traits::{OnSwap, Registrar}; pub use pallet::*; use parity_scale_codec::{Decode, Encode}; -use runtime_parachains::paras::{OnNewHead, ParaKind}; +use runtime_parachains::paras::{OnNewHead, ParaKind, UpgradeRequirements}; use scale_info::TypeInfo; use sp_runtime::{ - traits::{CheckedSub, Saturating}, + traits::{AccountIdConversion, CheckedSub, Saturating}, RuntimeDebug, }; #[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug, TypeInfo)] pub struct ParaInfo { - /// The account that has placed a deposit for registering this para. + /// The account that has initially placed a deposit for registering this para. + /// + /// 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(crate) manager: Account, - /// The amount reserved by the `manager` account for the registration. + /// The total amount reserved for this para. deposit: Balance, /// Whether the para registration should be locked from being controlled by the manager. /// None means the lock had not been explicitly set, and should be treated as false. locked: Option, + /// The billing account for a parachain. This account is responsible for holding the required + /// deposit and covering all associated costs related to scheduling parachain validation code + /// upgrades. + /// + /// This account must be explicitly set using the `set_parachain_billing_account_to_self` + /// extrinsic + /// + /// None indicates that the billing account hasn't been set, and attempting to schedule a + /// parachain upgrade will result in failure. + billing_account: Option, + /// The deposit that will be refunded upon a successful code upgrade. + pending_deposit_refund: Option, } impl ParaInfo { @@ -62,6 +79,15 @@ impl ParaInfo { } } +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum CodeUpgradeScheduleError { + /// The parachain billing account has to be explicitly set before being able to schedule a + /// code upgrade. + BillingAccountNotSet, + /// Failed to pay the associated costs of scheduling the code upgrade. + FailedToPayUpgradeCosts, +} + type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; @@ -71,8 +97,12 @@ pub trait WeightInfo { fn force_register() -> Weight; fn deregister() -> Weight; fn swap() -> Weight; + fn pre_code_upgrade() -> Weight; + fn on_code_upgraded() -> Weight; fn schedule_code_upgrade(b: u32) -> Weight; fn set_current_head(b: u32) -> Weight; + fn set_parachain_billing_account_to_self() -> Weight; + fn force_set_parachain_billing_account() -> Weight; } pub struct TestWeightInfo; @@ -92,12 +122,24 @@ impl WeightInfo for TestWeightInfo { fn swap() -> Weight { Weight::zero() } + fn pre_code_upgrade() -> Weight { + Weight::zero() + } + fn on_code_upgraded() -> Weight { + Weight::zero() + } fn schedule_code_upgrade(_b: u32) -> Weight { Weight::zero() } fn set_current_head(_b: u32) -> Weight { Weight::zero() } + fn set_parachain_billing_account_to_self() -> Weight { + Weight::zero() + } + fn force_set_parachain_billing_account() -> Weight { + Weight::zero() + } } #[frame_support::pallet] @@ -107,7 +149,7 @@ pub mod pallet { use frame_system::pallet_prelude::*; /// The current storage version. - const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + const STORAGE_VERSION: StorageVersion = StorageVersion::new(2); #[pallet::pallet] #[pallet::without_storage_info] @@ -142,6 +184,12 @@ pub mod pallet { #[pallet::constant] type DataDepositPerByte: Get>; + /// The fee required to perform a validation code upgrade for a parachain. + /// + /// This is used to discourage spamming parachain upgrades. + #[pallet::constant] + type UpgradeFee: Get>; + /// Weight Information for the Extrinsics in the Pallet type WeightInfo: WeightInfo; } @@ -153,6 +201,12 @@ pub mod pallet { Deregistered { para_id: ParaId }, Reserved { para_id: ParaId, who: T::AccountId }, Swapped { para_id: ParaId, other_id: ParaId }, + BillingAccountSet { para_id: ParaId, who: T::AccountId }, + Refunded { para_id: ParaId, who: T::AccountId, amount: BalanceOf }, + // NOTE: This event won't be emitted if the upgrade is scheduled using the extrinsic, + // since failed dispatchables can't emit events. This is useful for upgrades that are + // triggered from the parachain inclusion pallet, which is almost always the case anyway. + CodeUpgradeScheduleFailed(CodeUpgradeScheduleError), } #[pallet::error] @@ -193,10 +247,8 @@ pub mod pallet { #[pallet::storage] pub(super) type PendingSwap = StorageMap<_, Twox64Concat, ParaId, ParaId>; - /// 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.) + /// Stores all the registration, code upgrade permission, and billing-related information for + /// the parachain #[pallet::storage] pub type Paras = StorageMap<_, Twox64Concat, ParaId, ParaInfo>>; @@ -402,8 +454,16 @@ pub mod pallet { /// Schedule a parachain upgrade. /// - /// Can be called by Root, the parachain, or the parachain manager if the parachain is - /// unlocked. + /// ## Arguments + /// - `origin`: Can be called by Root, the parachain, or the parachain manager if the + /// parachain is unlocked. + /// - `para`: The parachain's ID for which the code upgrade is scheduled. + /// - `new_code`: The new validation code of the parachain. + /// + /// ## Deposits/Fees + /// In case the call is made by the parachain manager or the parachain itself, there will be + /// associated upgrade costs. Depending on the size of the new validation code, the caller's + /// reserved deposit might be adjusted to account for the size difference. #[pallet::call_index(7)] #[pallet::weight(::WeightInfo::schedule_code_upgrade(new_code.0.len() as u32))] pub fn schedule_code_upgrade( @@ -411,9 +471,17 @@ pub mod pallet { para: ParaId, new_code: ValidationCode, ) -> DispatchResult { - Self::ensure_root_para_or_owner(origin, para)?; - runtime_parachains::schedule_code_upgrade::(para, new_code, SetGoAhead::No)?; - Ok(()) + Self::ensure_root_para_or_owner(origin.clone(), para)?; + + let requirements = if ensure_root(origin.clone()).is_ok() { + // Upgrades initiated by the root origin do not have any upgrade cost-related + // requirements. For this reason we can skip all the pre code upgrade checks. + UpgradeRequirements::SkipRequirements + } else { + UpgradeRequirements::EnforceRequirements + }; + + Self::do_schedule_code_upgrade(para, new_code, requirements) } /// Set the parachain's current head. @@ -431,6 +499,74 @@ pub mod pallet { runtime_parachains::set_current_head::(para, new_head); Ok(()) } + + /// Changes the billing account of a parachain to the caller. + /// + /// ## Arguments + /// - `origin`: Can be called by Root, the parachain, or the parachain manager if the + /// parachain is unlocked. + /// - `para`: The parachain's ID for which the billing account is set. + /// + /// ## Deposits/Fees + /// For this call to be successful, the caller account must have a sufficient balance + /// to cover the current deposit required for this parachain. + /// + /// ## Events + /// The `BillingAccountSet` event is emitted in case of success. + #[pallet::call_index(9)] + #[pallet::weight(::WeightInfo::set_parachain_billing_account_to_self())] + pub fn set_parachain_billing_account_to_self( + origin: OriginFor, + para: ParaId, + ) -> DispatchResult { + Self::ensure_root_para_or_owner(origin.clone(), para)?; + + let new_billing_account = if let Ok(caller) = ensure_signed(origin.clone()) { + let para_info = Paras::::get(para).ok_or(Error::::NotRegistered)?; + ensure!(!para_info.is_locked(), Error::::ParaLocked); + ensure!(para_info.manager == caller, Error::::NotOwner); + + caller + } else if ensure_root(origin).is_ok() { + // Root should use `force_set_parachain_billing_account` since it doesn't make sense + // for the root to set itself as the billing account. + return Ok(()) + } else { + para.into_account_truncating() + }; + + Self::set_parachain_billing_account(para, new_billing_account)?; + + Ok(()) + } + + /// Sets the billing account of a parachain to a specific account. + /// + /// ## Arguments + /// - `origin`: Must be Root. + /// - `para`: The parachain's ID for which the billing account is set. + /// - `billing_account`: The account that will be responsible for covering all parachain + /// related costs. + /// + /// ## Deposits/Fees + /// For this call to be successful, the `billing_account` account must have a sufficient + /// balance to cover the current deposit required for this parachain. + /// + /// ## Events + /// The `BillingAccountSet` event is emitted in case of success. + #[pallet::call_index(10)] + #[pallet::weight(::WeightInfo::force_set_parachain_billing_account())] + pub fn force_set_parachain_billing_account( + origin: OriginFor, + para: ParaId, + billing_account: T::AccountId, + ) -> DispatchResult { + ensure_root(origin)?; + + Self::set_parachain_billing_account(para, billing_account)?; + + Ok(()) + } } } @@ -578,7 +714,13 @@ impl Pallet { let deposit = deposit_override.unwrap_or_else(T::ParaDeposit::get); ::Currency::reserve(&who, deposit)?; - let info = ParaInfo { manager: who.clone(), deposit, locked: None }; + let info = ParaInfo { + manager: who.clone(), + deposit, + locked: None, + billing_account: None, + pending_deposit_refund: None, + }; Paras::::insert(id, info); Self::deposit_event(Event::::Reserved { para_id: id, who }); @@ -613,7 +755,13 @@ impl Pallet { } else if let Some(rebate) = deposited.checked_sub(&deposit) { ::Currency::unreserve(&who, rebate); }; - let info = ParaInfo { manager: who.clone(), deposit, locked: None }; + let info = ParaInfo { + manager: who.clone(), + deposit, + locked: None, + billing_account: None, + pending_deposit_refund: None, + }; Paras::::insert(id, info); // We check above that para has no lifecycle, so this should not fail. @@ -642,6 +790,31 @@ impl Pallet { Ok(()) } + /// Schedules a code upgrade for a parachain. + /// + /// If `requirements` is set to `UpgradeRequirements::SkipRequirements` all the code upgrade + /// cost related requirements will be ignored. + /// + /// If the size of the validation is reduced and the upgrade is successful the caller will be + /// eligible for receiving back a portion of their deposit that is no longer required. + fn do_schedule_code_upgrade( + para: ParaId, + new_code: ValidationCode, + requirements: UpgradeRequirements, + ) -> DispatchResult { + // Before doing anything we ensure that a code upgrade is allowed at the moment for the + // specific parachain. + ensure!(paras::Pallet::::can_upgrade_validation_code(para), Error::::CannotUpgrade); + + ensure!( + T::PreCodeUpgrade::pre_code_upgrade(para, new_code.clone(), requirements).is_ok(), + Error::::CannotUpgrade + ); + runtime_parachains::schedule_code_upgrade::(para, new_code, SetGoAhead::No)?; + + Ok(()) + } + /// Verifies the onboarding data is valid for a para. /// /// Returns `ParaGenesisArgs` and the deposit needed for the data. @@ -658,10 +831,7 @@ impl Pallet { Error::::HeadDataTooLarge ); - 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())); + let deposit = Self::required_para_deposit(genesis_head.0.len(), validation_code.0.len()); Ok((ParaGenesisArgs { genesis_head, validation_code, para_kind }, deposit)) } @@ -675,6 +845,63 @@ impl Pallet { debug_assert!(res2.is_ok()); T::OnSwap::on_swap(to_upgrade, to_downgrade); } + + /// Sets the billing account of a parachain. + /// + /// The newly set account will be responsible for covering all associated costs related to + /// performing parachain validation code upgrades. + fn set_parachain_billing_account( + para: ParaId, + new_billing_account: T::AccountId, + ) -> DispatchResult { + let mut info = Paras::::get(para).ok_or_else(|| Error::::NotRegistered)?; + + // When updating the account responsible for all code upgrade costs, we unreserve all + // funds associated with the registered parachain from the original billing account and + // reserves the required amount from the new one. + + ::Currency::reserve(&new_billing_account, info.deposit)?; + + // After reserving the required funds from the new billing account we can now safely + // refund the current deposit holder. + let current_deposit_holder = info.billing_account.clone().unwrap_or(info.manager.clone()); + ::Currency::unreserve(¤t_deposit_holder, info.deposit); + + info.billing_account = Some(new_billing_account.clone()); + Paras::::insert(para, info); + + Self::deposit_event(Event::::BillingAccountSet { + para_id: para, + who: new_billing_account, + }); + Ok(()) + } + + #[transactional] + fn charge_upgrade_costs( + billing_account: T::AccountId, + additional_deposit: BalanceOf, + ) -> DispatchResult { + ::Currency::withdraw( + &billing_account, + T::UpgradeFee::get(), + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive, + )?; + + ::Currency::reserve(&billing_account, additional_deposit)?; + + Ok(()) + } + + /// Returns the required deposit amount for the parachain, given the specified genesis head + /// and validation code size. + fn required_para_deposit(head_size: usize, validation_code_size: usize) -> BalanceOf { + let per_byte_fee = T::DataDepositPerByte::get(); + T::ParaDeposit::get() + .saturating_add(per_byte_fee.saturating_mul((head_size as u32).into())) + .saturating_add(per_byte_fee.saturating_mul((validation_code_size as u32).into())) + } } impl OnNewHead for Pallet { @@ -692,6 +919,106 @@ impl OnNewHead for Pallet { } } +impl PreCodeUpgrade for Pallet { + /// Ensures that all upgrade-related costs are covered for the specific parachain. Upon success, + /// it updates the deposit-related state for the parachain. + /// + /// There are three cases where we do not charge any upgrade costs from the initiator + /// of the upgrade: + /// + /// 1. `requirements` is explicitly set to `UpgradeRequirements::SkipRequirements`. + /// + /// 2. System parachains do not pay for upgrade fees. + /// + /// 3. All lease-holding parachains are permitted to do upgrades for free. This is introduced to + /// avoid causing a breaking change to the system once para upgrade fees are required. + fn pre_code_upgrade( + para: ParaId, + new_code: ValidationCode, + requirements: UpgradeRequirements, + ) -> Result { + let Some(head) = paras::Pallet::::para_head(para) else { + return Err(T::DbWeight::get().reads(1)) + }; + + let Some(mut info) = Paras::::get(para) else { return Err(T::DbWeight::get().reads(2)) }; + + let new_deposit = Self::required_para_deposit(head.0.len(), new_code.0.len()); + let current_deposit = info.deposit; + + let lease_holding = Self::is_parachain(para); + let free_upgrade = requirements == UpgradeRequirements::SkipRequirements || + para.is_system() || + lease_holding; + + if !free_upgrade { + let Some(billing_account) = info.billing_account.clone() else { + Self::deposit_event(Event::::CodeUpgradeScheduleFailed( + CodeUpgradeScheduleError::BillingAccountNotSet, + )); + // An overestimate of the used weight, but it's better to be safe than sorry. + return Err(::WeightInfo::pre_code_upgrade()) + }; + + let additional_deposit = new_deposit.saturating_sub(current_deposit); + if Self::charge_upgrade_costs(billing_account, additional_deposit).is_err() { + Self::deposit_event(Event::::CodeUpgradeScheduleFailed( + CodeUpgradeScheduleError::FailedToPayUpgradeCosts, + )); + // An overestimate of the used weight, but it's better to be safe than sorry. + return Err(::WeightInfo::pre_code_upgrade()) + }; + + // Update the deposit to the new appropriate amount. + info.deposit = new_deposit; + } + + if current_deposit > new_deposit { + // The billing account should receive a refund if the current deposit exceeds the new + // required deposit, even if they did not initiate the upgrade. + // + // The excess deposit will be refunded to the caller upon the success of the code + // upgrade. + // + // The reason why the deposit is not instantly refunded is that scheduling a code + // upgrade doesn't guarantee the success of it. + // + // If we returned the deposit here, a possible attack scenario would be to register + // the validation code of a parachain and then schedule a code upgrade to set the + // code to an empty blob. In such a case, the pre-checking process would fail, so + // the old code would remain on-chain even though there is no deposit to cover it. + + info.pending_deposit_refund = Some(current_deposit.saturating_sub(new_deposit)); + } + + Paras::::insert(para, info); + Ok(::WeightInfo::pre_code_upgrade()) + } +} + +impl OnCodeUpgraded for Pallet { + fn on_code_upgraded(id: ParaId) -> Weight { + let Some(mut info) = Paras::::get(id) else { return T::DbWeight::get().reads(1) }; + + if let Some(rebate) = info.pending_deposit_refund { + if let Some(billing_account) = info.billing_account.clone() { + ::Currency::unreserve(&billing_account, rebate); + info.pending_deposit_refund = None; + Paras::::insert(id, info); + + Self::deposit_event(Event::::Refunded { + para_id: id, + who: billing_account, + amount: rebate, + }); + return ::WeightInfo::on_code_upgraded() + } + } + + T::DbWeight::get().reads(1) + } +} + #[cfg(test)] mod tests { use super::*; @@ -815,6 +1142,8 @@ mod tests { type UnsignedPriority = ParasUnsignedPriority; type QueueFootprinter = (); type NextSessionRotation = crate::mock::TestNextSessionRotation; + type PreCodeUpgrade = Registrar; + type OnCodeUpgraded = (); type OnNewHead = (); type AssignCoretime = (); } @@ -827,6 +1156,7 @@ mod tests { pub const ParaDeposit: Balance = 10; pub const DataDepositPerByte: Balance = 1; pub const MaxRetries: u32 = 3; + pub const UpgradeFee: Balance = 2; } impl Config for Test { @@ -836,6 +1166,7 @@ mod tests { type OnSwap = MockSwap; type ParaDeposit = ParaDeposit; type DataDepositPerByte = DataDepositPerByte; + type UpgradeFee = UpgradeFee; type WeightInfo = TestWeightInfo; } @@ -965,7 +1296,7 @@ mod tests { test_genesis_head(32), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); run_to_session(START_SESSION_INDEX + 2); // It is now a parathread (on-demand parachain). @@ -1009,7 +1340,7 @@ mod tests { test_genesis_head(32), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); run_to_session(START_SESSION_INDEX + 2); assert!(Parachains::is_parathread(para_id)); @@ -1119,7 +1450,7 @@ mod tests { test_genesis_head(32), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); run_to_session(START_SESSION_INDEX + 2); assert!(Parachains::is_parathread(para_id)); @@ -1147,7 +1478,7 @@ mod tests { test_genesis_head(32), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); run_to_session(START_SESSION_INDEX + 2); assert!(Parachains::is_parathread(para_id)); @@ -1188,7 +1519,7 @@ mod tests { test_genesis_head(max_head_size() as usize), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); run_to_session(START_SESSION_INDEX + 2); @@ -1253,7 +1584,12 @@ mod tests { test_genesis_head(max_head_size() as usize), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX + 6); + conclude_pvf_checking::( + &validation_code, + VALIDATORS, + START_SESSION_INDEX + 6, + true, + ); run_to_session(START_SESSION_INDEX + 8); @@ -1361,7 +1697,7 @@ mod tests { test_genesis_head(32), validation_code.clone(), )); - conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX); + conclude_pvf_checking::(&validation_code, VALIDATORS, START_SESSION_INDEX, true); // Cannot swap assert_ok!(Registrar::swap(RuntimeOrigin::root(), para_1, para_2)); @@ -1593,10 +1929,44 @@ mod benchmarking { } schedule_code_upgrade { + // The 'worst case' scenario in terms of benchmarking is when the upgrade isn't free + // and there is a refund. + + let para = register_para::(LOWEST_PUBLIC_ID.into()); + let b in 1 .. MAX_CODE_SIZE; let new_code = ValidationCode(vec![0; b as usize]); - let para_id = ParaId::from(1000); - }: _(RawOrigin::Root, para_id, new_code) + + // Actually finish registration process + next_scheduled_session::(); + // Set the billing account + let caller: T::AccountId = whitelisted_caller(); + assert_ok!(Registrar::::force_set_parachain_billing_account(RawOrigin::Root.into(), para, caller.clone())); + }: _(RawOrigin::Signed(caller), para, new_code) + + pre_code_upgrade { + let para = register_para::(LOWEST_PUBLIC_ID.into()); + let new_small_code = ValidationCode(vec![0]); + + // Actually finish registration process + next_scheduled_session::(); + }: { + let _ = T::PreCodeUpgrade::pre_code_upgrade(para, new_small_code, UpgradeRequirements::EnforceRequirements); + } + + on_code_upgraded { + let para = register_para::(LOWEST_PUBLIC_ID.into()); + let new_small_code = ValidationCode(vec![0]); + + // Actually finish registration process + next_scheduled_session::(); + // Set the billing account + assert_ok!(Registrar::::force_set_parachain_billing_account(RawOrigin::Root.into(), para, whitelisted_caller())); + + assert_ok!(Registrar::::schedule_code_upgrade(RawOrigin::Root.into(), para, new_small_code)); + }: { + let _ = T::OnCodeUpgraded::on_code_upgraded(para); + } set_current_head { let b in 1 .. MAX_HEAD_DATA_SIZE; @@ -1604,6 +1974,28 @@ mod benchmarking { let para_id = ParaId::from(1000); }: _(RawOrigin::Root, para_id, new_head) + set_parachain_billing_account_to_self { + let para = register_para::(LOWEST_PUBLIC_ID.into()); + // Actually finish registration process + next_scheduled_session::(); + + let caller: T::AccountId = whitelisted_caller(); + }: _(RawOrigin::Signed(caller.clone()), para) + verify { + assert_eq!(Paras::::get(para).unwrap().billing_account, Some(caller)); + } + + force_set_parachain_billing_account { + let para = register_para::(LOWEST_PUBLIC_ID.into()); + // Actually finish registration process + next_scheduled_session::(); + + let caller: T::AccountId = whitelisted_caller(); + }: _(RawOrigin::Root, para, caller.clone()) + verify { + assert_eq!(Paras::::get(para).unwrap().billing_account, Some(caller)); + } + impl_benchmark_test_suite!( Registrar, crate::integration_tests::new_test_ext(), diff --git a/polkadot/runtime/parachains/src/inclusion/mod.rs b/polkadot/runtime/parachains/src/inclusion/mod.rs index 90af9cde00a8..8f1b73866ee0 100644 --- a/polkadot/runtime/parachains/src/inclusion/mod.rs +++ b/polkadot/runtime/parachains/src/inclusion/mod.rs @@ -22,7 +22,7 @@ use crate::{ configuration::{self, HostConfiguration}, disputes, dmp, hrmp, - paras::{self, SetGoAhead}, + paras::{self, PreCodeUpgrade, SetGoAhead, UpgradeRequirements}, scheduler::{self, AvailabilityTimeoutStatus}, shared::{self, AllowedRelayParentsTracker}, }; @@ -879,15 +879,10 @@ impl Pallet { // initial weight is config read. let mut weight = T::DbWeight::get().reads_writes(1, 0); if let Some(new_code) = commitments.new_validation_code { - // Block number of candidate's inclusion. - let now = >::block_number(); - - weight.saturating_add(>::schedule_code_upgrade( + weight.saturating_accrue(Self::try_schedule_code_upgrade( + &config, receipt.descriptor.para_id, new_code, - now, - &config, - SetGoAhead::Yes, )); } @@ -1124,6 +1119,42 @@ impl Pallet { ) -> Option>> { >::get(¶) } + + /// Attempts to schedule a code upgrade. + /// + /// Returns the consumed weight. + fn try_schedule_code_upgrade( + config: &HostConfiguration>, + para_id: ParaId, + new_code: primitives::ValidationCode, + ) -> Weight { + // Block number of candidate's inclusion. + let now = >::block_number(); + + match ::PreCodeUpgrade::pre_code_upgrade( + para_id, + new_code.clone(), + UpgradeRequirements::EnforceRequirements, + ) { + Ok(consumed_weight) => + consumed_weight.saturating_add(>::schedule_code_upgrade( + para_id, + new_code, + now, + &config, + SetGoAhead::Yes, + )), + Err(consumed_weight) => { + >::signal_code_upgrade_failure(para_id); + log::debug!( + target: LOG_TARGET, + "Failed to schedule a code upgrade for parachain {}", + u32::from(para_id), + ); + consumed_weight + }, + } + } } const fn availability_threshold(n_validators: usize) -> usize { diff --git a/polkadot/runtime/parachains/src/mock.rs b/polkadot/runtime/parachains/src/mock.rs index e3fcf7dd603f..a23cf44b3c52 100644 --- a/polkadot/runtime/parachains/src/mock.rs +++ b/polkadot/runtime/parachains/src/mock.rs @@ -241,6 +241,8 @@ impl crate::paras::Config for Test { type UnsignedPriority = ParasUnsignedPriority; type QueueFootprinter = ParaInclusion; type NextSessionRotation = TestNextSessionRotation; + type PreCodeUpgrade = (); + type OnCodeUpgraded = (); type OnNewHead = (); type AssignCoretime = (); } diff --git a/polkadot/runtime/parachains/src/paras/mod.rs b/polkadot/runtime/parachains/src/paras/mod.rs index e97df8e4a2b3..6ab29847e355 100644 --- a/polkadot/runtime/parachains/src/paras/mod.rs +++ b/polkadot/runtime/parachains/src/paras/mod.rs @@ -415,6 +415,12 @@ enum PvfCheckOutcome { Rejected, } +#[derive(Debug, Copy, Clone, PartialEq, TypeInfo, Decode, Encode)] +pub enum UpgradeRequirements { + SkipRequirements, + EnforceRequirements, +} + /// This struct describes the current state of an in-progress PVF pre-checking vote. #[derive(Encode, Decode, TypeInfo)] pub(crate) struct PvfCheckActiveVoteState { @@ -506,6 +512,46 @@ impl OnNewHead for Tuple { } } +/// A callback for when a parachain successfully upgraded its code. +pub trait OnCodeUpgraded { + /// A function to execute some custom logic once the pre-checking is successfully completed. + fn on_code_upgraded(id: ParaId) -> Weight; +} + +/// An empty implementation of the trait where there is no logic executed upon a successful +/// code upgrade. +impl OnCodeUpgraded for () { + fn on_code_upgraded(_id: ParaId) -> Weight { + Weight::zero() + } +} + +pub trait PreCodeUpgrade { + /// A function that performs custom logic to before sceduling a code upgrade. + /// + /// `requirements` signals whether to enforce the pre code upgrade requirements. + /// + /// As a result, it indicates either the success or failure of executing the pre code upgrade + /// scheduling logic. In both cases, it returns the consumed weight. + fn pre_code_upgrade( + id: ParaId, + new_code: ValidationCode, + requirements: UpgradeRequirements, + ) -> Result; +} + +/// An empty implementation of the trait where there are no checks performed before scheduling a +/// code upgrade. +impl PreCodeUpgrade for () { + fn pre_code_upgrade( + _id: ParaId, + _new_code: ValidationCode, + _requirements: UpgradeRequirements, + ) -> Result { + Ok(Weight::zero()) + } +} + /// Assign coretime to some parachain. /// /// This assigns coretime to a parachain without using the coretime chain. Thus, this should only be @@ -618,6 +664,13 @@ pub mod pallet { /// Runtime hook for when a parachain head is updated. type OnNewHead: OnNewHead; + /// A type that performs custom logic to determine whether a code upgrade is allowed to be + /// performed. + type PreCodeUpgrade: PreCodeUpgrade; + + /// Type that executes some custom logic upon a successful code upgrade. + type OnCodeUpgraded: OnCodeUpgraded; + /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; @@ -772,7 +825,7 @@ pub mod pallet { pub(super) type FutureCodeHash = StorageMap<_, Twox64Concat, ParaId, ValidationCodeHash>; - /// This is used by the relay-chain to communicate to a parachain a go-ahead with in the upgrade + /// This is used by the relay-chain to communicate to a parachain a go-ahead within the upgrade /// procedure. /// /// This value is absent when there are no upgrades scheduled or during the time the relay chain @@ -1245,13 +1298,13 @@ impl Pallet { } /// Called by the initializer to initialize the paras pallet. - pub(crate) fn initializer_initialize(now: BlockNumberFor) -> Weight { + pub fn initializer_initialize(now: BlockNumberFor) -> Weight { let weight = Self::prune_old_code(now); weight + Self::process_scheduled_upgrade_changes(now) } /// Called by the initializer to finalize the paras pallet. - pub(crate) fn initializer_finalize(now: BlockNumberFor) { + pub fn initializer_finalize(now: BlockNumberFor) { Self::process_scheduled_upgrade_cooldowns(now); } @@ -1267,7 +1320,7 @@ impl Pallet { } /// The validation code of live para. - pub(crate) fn current_code(para_id: &ParaId) -> Option { + pub fn current_code(para_id: &ParaId) -> Option { Self::current_code_hash(para_id).and_then(|code_hash| { let code = CodeByHash::::get(&code_hash); if code.is_none() { @@ -2058,7 +2111,10 @@ impl Pallet { let now = >::block_number(); let weight = if let Some(prior_code_hash) = maybe_prior_code_hash { - Self::note_past_code(id, expected_at, now, prior_code_hash) + let mut weight = Self::note_past_code(id, expected_at, now, prior_code_hash); + weight = weight.saturating_add(T::OnCodeUpgraded::on_code_upgraded(id)); + + weight } else { log::error!(target: LOG_TARGET, "Missing prior code hash for para {:?}", &id); Weight::zero() @@ -2081,6 +2137,11 @@ impl Pallet { weight.saturating_add(T::OnNewHead::on_new_head(id, &new_head)) } + /// Signal to the parachain that scheduling the code upgrade failed. + pub(crate) fn signal_code_upgrade_failure(id: ParaId) { + UpgradeGoAheadSignal::::insert(&id, UpgradeGoAhead::Abort); + } + /// Returns the list of PVFs (aka validation code) that require casting a vote by a validator in /// the active validator set. pub(crate) fn pvfs_require_precheck() -> Vec { @@ -2154,7 +2215,7 @@ impl Pallet { /// If a candidate from the specified parachain were submitted at the current block, this /// function returns if that candidate passes the acceptance criteria. - pub(crate) fn can_upgrade_validation_code(id: ParaId) -> bool { + pub fn can_upgrade_validation_code(id: ParaId) -> bool { FutureCodeHash::::get(&id).is_none() && UpgradeRestrictionSignal::::get(&id).is_none() } diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 51c00336bef7..2e8713d7c5df 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -942,6 +942,8 @@ impl parachains_paras::Config for Runtime { type UnsignedPriority = ParasUnsignedPriority; type QueueFootprinter = ParaInclusion; type NextSessionRotation = Babe; + type PreCodeUpgrade = Registrar; + type OnCodeUpgraded = Registrar; type OnNewHead = Registrar; type AssignCoretime = CoretimeAssignmentProvider; } @@ -1076,6 +1078,7 @@ impl parachains_slashing::Config for Runtime { parameter_types! { pub const ParaDeposit: Balance = 40 * UNITS; + pub const UpgradeFee: Balance = 2 * UNITS; } impl paras_registrar::Config for Runtime { @@ -1084,6 +1087,7 @@ impl paras_registrar::Config for Runtime { type Currency = Balances; type OnSwap = (Crowdloan, Slots); type ParaDeposit = ParaDeposit; + type UpgradeFee = UpgradeFee; type DataDepositPerByte = DataDepositPerByte; type WeightInfo = weights::runtime_common_paras_registrar::WeightInfo; } @@ -1635,7 +1639,7 @@ pub mod migrations { parachains_scheduler::migration::MigrateV1ToV2, parachains_configuration::migration::v8::MigrateToV8, parachains_configuration::migration::v9::MigrateToV9, - paras_registrar::migration::MigrateToV1, + paras_registrar::migration::v2::MigrateToV2, pallet_referenda::migration::v1::MigrateV0ToV1, pallet_referenda::migration::v1::MigrateV0ToV1, @@ -2264,7 +2268,7 @@ sp_api::impl_runtime_apis! { use sp_storage::TrackedStorageKey; use xcm::latest::prelude::*; use xcm_config::{ - AssetHub, LocalCheckAccount, LocationConverter, TokenLocation, XcmConfig, + AssetHub, LocalCheckAccount, TokenLocation, XcmConfig, }; parameter_types! { @@ -2320,7 +2324,7 @@ sp_api::impl_runtime_apis! { } impl pallet_xcm_benchmarks::Config for Runtime { type XcmConfig = XcmConfig; - type AccountIdConverter = LocationConverter; + type AccountIdConverter = xcm_config::LocationConverter; type DeliveryHelper = runtime_common::xcm_sender::ToParachainDeliveryHelper< XcmConfig, ExistentialDepositAsset, diff --git a/polkadot/runtime/rococo/src/weights/runtime_common_paras_registrar.rs b/polkadot/runtime/rococo/src/weights/runtime_common_paras_registrar.rs index 0a56562a1a95..cbb3cb351e72 100644 --- a/polkadot/runtime/rococo/src/weights/runtime_common_paras_registrar.rs +++ b/polkadot/runtime/rococo/src/weights/runtime_common_paras_registrar.rs @@ -218,4 +218,60 @@ impl runtime_common::paras_registrar::WeightInfo for We .saturating_add(Weight::from_parts(855, 0).saturating_mul(b.into())) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: Paras Heads (r:0 w:1) + /// Proof Skipped: Paras Heads (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[1, 1048576]`. + fn pre_code_upgrade() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_804_000 picoseconds. + Weight::from_parts(8_956_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + // Standard Error: 3 + .saturating_add(Weight::from_parts(1_029, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: Paras Heads (r:0 w:1) + /// Proof Skipped: Paras Heads (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[1, 1048576]`. + fn on_code_upgraded() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_804_000 picoseconds. + Weight::from_parts(8_956_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + // Standard Error: 3 + .saturating_add(Weight::from_parts(1_029, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: Paras Heads (r:0 w:1) + /// Proof Skipped: Paras Heads (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[1, 1048576]`. + fn set_parachain_billing_account_to_self() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_804_000 picoseconds. + Weight::from_parts(8_956_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + // Standard Error: 3 + .saturating_add(Weight::from_parts(1_029, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: Paras Heads (r:0 w:1) + /// Proof Skipped: Paras Heads (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[1, 1048576]`. + fn force_set_parachain_billing_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_804_000 picoseconds. + Weight::from_parts(8_956_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + // Standard Error: 3 + .saturating_add(Weight::from_parts(1_029, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } } diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index 18bcb16834e2..cfb89c1d2fc6 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -539,6 +539,8 @@ impl parachains_paras::Config for Runtime { type UnsignedPriority = ParasUnsignedPriority; type QueueFootprinter = ParaInclusion; type NextSessionRotation = Babe; + type PreCodeUpgrade = (); + type OnCodeUpgraded = (); type OnNewHead = (); type AssignCoretime = (); } diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 941193ba9414..e4f6e0dee91f 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1143,6 +1143,8 @@ impl parachains_paras::Config for Runtime { type UnsignedPriority = ParasUnsignedPriority; type QueueFootprinter = ParaInclusion; type NextSessionRotation = Babe; + type PreCodeUpgrade = (); + type OnCodeUpgraded = (); type OnNewHead = (); type AssignCoretime = (); } @@ -1273,6 +1275,7 @@ impl parachains_slashing::Config for Runtime { parameter_types! { pub const ParaDeposit: Balance = 2000 * CENTS; + pub const UpgradeFee: Balance = 0; pub const RegistrarDataDepositPerByte: Balance = deposit(0, 1); } @@ -1283,6 +1286,7 @@ impl paras_registrar::Config for Runtime { type OnSwap = (Crowdloan, Slots); type ParaDeposit = ParaDeposit; type DataDepositPerByte = RegistrarDataDepositPerByte; + type UpgradeFee = UpgradeFee; type WeightInfo = weights::runtime_common_paras_registrar::WeightInfo; } @@ -1645,7 +1649,7 @@ pub mod migrations { parachains_scheduler::migration::MigrateV1ToV2, parachains_configuration::migration::v8::MigrateToV8, parachains_configuration::migration::v9::MigrateToV9, - paras_registrar::migration::MigrateToV1, + paras_registrar::migration::v2::MigrateToV2, pallet_referenda::migration::v1::MigrateV0ToV1, pallet_grandpa::migrations::MigrateV4ToV5, parachains_configuration::migration::v10::MigrateToV10, diff --git a/polkadot/runtime/westend/src/weights/runtime_common_paras_registrar.rs b/polkadot/runtime/westend/src/weights/runtime_common_paras_registrar.rs index 50290c0fe59f..2b669156cd81 100644 --- a/polkadot/runtime/westend/src/weights/runtime_common_paras_registrar.rs +++ b/polkadot/runtime/westend/src/weights/runtime_common_paras_registrar.rs @@ -215,4 +215,61 @@ impl runtime_common::paras_registrar::WeightInfo for We .saturating_add(Weight::from_parts(1_029, 0).saturating_mul(b.into())) .saturating_add(T::DbWeight::get().writes(1)) } + + /// Storage: Paras Heads (r:0 w:1) + /// Proof Skipped: Paras Heads (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[1, 1048576]`. + fn pre_code_upgrade() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_804_000 picoseconds. + Weight::from_parts(8_956_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + // Standard Error: 3 + .saturating_add(Weight::from_parts(1_029, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: Paras Heads (r:0 w:1) + /// Proof Skipped: Paras Heads (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[1, 1048576]`. + fn on_code_upgraded() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_804_000 picoseconds. + Weight::from_parts(8_956_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + // Standard Error: 3 + .saturating_add(Weight::from_parts(1_029, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: Paras Heads (r:0 w:1) + /// Proof Skipped: Paras Heads (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[1, 1048576]`. + fn set_parachain_billing_account_to_self() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_804_000 picoseconds. + Weight::from_parts(8_956_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + // Standard Error: 3 + .saturating_add(Weight::from_parts(1_029, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: Paras Heads (r:0 w:1) + /// Proof Skipped: Paras Heads (max_values: None, max_size: None, mode: Measured) + /// The range of component `b` is `[1, 1048576]`. + fn force_set_parachain_billing_account() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 8_804_000 picoseconds. + Weight::from_parts(8_956_000, 0) + .saturating_add(Weight::from_parts(0, 0)) + // Standard Error: 3 + .saturating_add(Weight::from_parts(1_029, 0)) + .saturating_add(T::DbWeight::get().writes(1)) + } } diff --git a/polkadot/zombienet_tests/smoke/0002-parachains-upgrade-smoke-test.toml b/polkadot/zombienet_tests/smoke/0002-parachains-upgrade-smoke-test.toml index d72e3ebdb335..5c44c9217724 100644 --- a/polkadot/zombienet_tests/smoke/0002-parachains-upgrade-smoke-test.toml +++ b/polkadot/zombienet_tests/smoke/0002-parachains-upgrade-smoke-test.toml @@ -4,7 +4,7 @@ bootnode = true [relaychain] default_image = "{{ZOMBIENET_INTEGRATION_TEST_IMAGE}}" -chain = "rococo-local" +chain = "westend-local" command = "polkadot" [[relaychain.nodes]] diff --git a/prdoc/pr_2372.prdoc b/prdoc/pr_2372.prdoc new file mode 100644 index 000000000000..619dd338501e --- /dev/null +++ b/prdoc/pr_2372.prdoc @@ -0,0 +1,23 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: On-demand & Coretime parachain upgrade fees + +doc: + - audience: Runtime User + description: | + All the parachains utilizing on-demand or the coretime model will be charged for performing + validation code upgrades. Before scheduling a code upgrade these parachains will have to + explicitly specify the account that will be charged for performing the code upgrade. + Existing lease holding and system chains are not affected by this change and are allowed + to continue performing code upgrades without associated upgrade costs. +migrations: + db: [] + runtime: + - reference: polkadot-runtime-parachains + description: | + ParaInfo has been updated to include two new fields: billing_account and pending_deposit_refund. + Both fields are initialized to None during the runtime migration. + +crates: + - name: polkadot-runtime-parachains