diff --git a/polkadot/node/core/approval-voting/src/approval_db/v1/mod.rs b/polkadot/node/core/approval-voting/src/approval_db/v1/mod.rs index 7397756f12..4f7c8b58d3 100644 --- a/polkadot/node/core/approval-voting/src/approval_db/v1/mod.rs +++ b/polkadot/node/core/approval-voting/src/approval_db/v1/mod.rs @@ -92,6 +92,8 @@ pub struct CandidateEntry { #[derive(Encode, Decode, Debug, Clone, PartialEq)] pub struct BlockEntry { pub block_hash: Hash, + pub block_number: BlockNumber, + pub parent_hash: Hash, pub session: SessionIndex, pub slot: Slot, /// Random bytes derived from the VRF submitted within the block by the block @@ -348,14 +350,14 @@ pub(crate) struct NewCandidateInfo { /// no information about new candidates will be referred to by this function. pub(crate) fn add_block_entry( store: &dyn KeyValueDB, - parent_hash: Hash, - number: BlockNumber, entry: BlockEntry, n_validators: usize, candidate_info: impl Fn(&CandidateHash) -> Option, ) -> Result> { let mut transaction = DBTransaction::new(); let session = entry.session; + let parent_hash = entry.parent_hash; + let number = entry.block_number; // Update the stored block range. { @@ -439,6 +441,45 @@ pub(crate) fn add_block_entry( Ok(candidate_entries) } +/// Forcibly approve all candidates included at up to the given relay-chain height in the indicated +/// chain. +pub fn force_approve( + store: &dyn KeyValueDB, + chain_head: Hash, + up_to: BlockNumber, +) -> Result<()> { + enum State { + WalkTo, + Approving, + } + + let mut cur_hash = chain_head; + let mut state = State::WalkTo; + + let mut tx = Transaction::default(); + + // iterate back to the `up_to` block, and then iterate backwards until all blocks + // are updated. + while let Some(mut entry) = load_block_entry(store, &cur_hash)? { + + if entry.block_number <= up_to { + state = State::Approving; + } + + cur_hash = entry.parent_hash; + + match state { + State::WalkTo => {}, + State::Approving => { + entry.approved_bitfield.iter_mut().for_each(|mut b| *b = true); + tx.put_block_entry(entry); + } + } + } + + tx.write(store) +} + // An atomic transaction of multiple candidate or block entries. #[derive(Default)] #[must_use = "Transactions do nothing unless written to a DB"] diff --git a/polkadot/node/core/approval-voting/src/approval_db/v1/tests.rs b/polkadot/node/core/approval-voting/src/approval_db/v1/tests.rs index 2167ad2281..199c672a27 100644 --- a/polkadot/node/core/approval-voting/src/approval_db/v1/tests.rs +++ b/polkadot/node/core/approval-voting/src/approval_db/v1/tests.rs @@ -57,10 +57,14 @@ fn make_bitvec(len: usize) -> BitVec { fn make_block_entry( block_hash: Hash, + parent_hash: Hash, + block_number: BlockNumber, candidates: Vec<(CoreIndex, CandidateHash)>, ) -> BlockEntry { BlockEntry { block_hash, + parent_hash, + block_number, session: 1, slot: Slot::from(1), relay_vrf_story: [0u8; 32], @@ -92,6 +96,8 @@ fn read_write() { let block_entry = make_block_entry( hash_a, + Default::default(), + 1, vec![(CoreIndex(0), candidate_hash)], ); @@ -155,18 +161,23 @@ fn add_block_entry_works() { let candidate_hash_a = CandidateHash(Hash::repeat_byte(3)); let candidate_hash_b = CandidateHash(Hash::repeat_byte(4)); + let block_number = 10; + let block_entry_a = make_block_entry( block_hash_a, + parent_hash, + block_number, vec![(CoreIndex(0), candidate_hash_a)], ); let block_entry_b = make_block_entry( block_hash_b, + parent_hash, + block_number, vec![(CoreIndex(0), candidate_hash_a), (CoreIndex(1), candidate_hash_b)], ); let n_validators = 10; - let block_number = 10; let mut new_candidate_info = HashMap::new(); new_candidate_info.insert(candidate_hash_a, NewCandidateInfo { @@ -177,8 +188,6 @@ fn add_block_entry_works() { add_block_entry( &store, - parent_hash, - block_number, block_entry_a.clone(), n_validators, |h| new_candidate_info.get(h).map(|x| x.clone()), @@ -192,8 +201,6 @@ fn add_block_entry_works() { add_block_entry( &store, - parent_hash, - block_number, block_entry_b.clone(), n_validators, |h| new_candidate_info.get(h).map(|x| x.clone()), @@ -219,11 +226,15 @@ fn add_block_entry_adds_child() { let mut block_entry_a = make_block_entry( block_hash_a, + parent_hash, + 1, Vec::new(), ); let block_entry_b = make_block_entry( block_hash_b, + block_hash_a, + 2, Vec::new(), ); @@ -231,8 +242,6 @@ fn add_block_entry_adds_child() { add_block_entry( &store, - parent_hash, - 1, block_entry_a.clone(), n_validators, |_| None, @@ -240,8 +249,6 @@ fn add_block_entry_adds_child() { add_block_entry( &store, - block_hash_a, - 2, block_entry_b.clone(), n_validators, |_| None, @@ -292,19 +299,33 @@ fn canonicalize_works() { let cand_hash_4 = CandidateHash(Hash::repeat_byte(13)); let cand_hash_5 = CandidateHash(Hash::repeat_byte(15)); - let block_entry_a = make_block_entry(block_hash_a, Vec::new()); - let block_entry_b1 = make_block_entry(block_hash_b1, Vec::new()); - let block_entry_b2 = make_block_entry(block_hash_b2, vec![(CoreIndex(0), cand_hash_1)]); - let block_entry_c1 = make_block_entry(block_hash_c1, Vec::new()); + let block_entry_a = make_block_entry(block_hash_a, genesis, 1, Vec::new()); + let block_entry_b1 = make_block_entry(block_hash_b1, block_hash_a, 2, Vec::new()); + let block_entry_b2 = make_block_entry( + block_hash_b2, + block_hash_a, + 2, + vec![(CoreIndex(0), cand_hash_1)], + ); + let block_entry_c1 = make_block_entry(block_hash_c1, block_hash_b1, 3, Vec::new()); let block_entry_c2 = make_block_entry( block_hash_c2, + block_hash_b2, + 3, vec![(CoreIndex(0), cand_hash_2), (CoreIndex(1), cand_hash_3)], ); let block_entry_d1 = make_block_entry( block_hash_d1, + block_hash_c1, + 4, vec![(CoreIndex(0), cand_hash_3), (CoreIndex(1), cand_hash_4)], ); - let block_entry_d2 = make_block_entry(block_hash_d2, vec![(CoreIndex(0), cand_hash_5)]); + let block_entry_d2 = make_block_entry( + block_hash_d2, + block_hash_c2, + 4, + vec![(CoreIndex(0), cand_hash_5)], + ); let candidate_info = { @@ -344,20 +365,18 @@ fn canonicalize_works() { // now insert all the blocks. let blocks = vec![ - (genesis, 1, block_entry_a.clone()), - (block_hash_a, 2, block_entry_b1.clone()), - (block_hash_a, 2, block_entry_b2.clone()), - (block_hash_b1, 3, block_entry_c1.clone()), - (block_hash_b2, 3, block_entry_c2.clone()), - (block_hash_c1, 4, block_entry_d1.clone()), - (block_hash_c2, 4, block_entry_d2.clone()), + block_entry_a.clone(), + block_entry_b1.clone(), + block_entry_b2.clone(), + block_entry_c1.clone(), + block_entry_c2.clone(), + block_entry_d1.clone(), + block_entry_d2.clone(), ]; - for (parent_hash, number, block_entry) in blocks { + for block_entry in blocks { add_block_entry( &store, - parent_hash, - number, block_entry, n_validators, |h| candidate_info.get(h).map(|x| x.clone()), @@ -446,3 +465,60 @@ fn canonicalize_works() { (block_hash_d2, None), ]); } + +#[test] +fn force_approve_works() { + let store = kvdb_memorydb::create(1); + let n_validators = 10; + + let mut tx = DBTransaction::new(); + write_stored_blocks(&mut tx, StoredBlockRange(1, 4)); + store.write(tx).unwrap(); + + let candidate_hash = CandidateHash(Hash::repeat_byte(42)); + let single_candidate_vec = vec![(CoreIndex(0), candidate_hash)]; + let candidate_info = { + let mut candidate_info = HashMap::new(); + candidate_info.insert(candidate_hash, NewCandidateInfo { + candidate: make_candidate(1.into(), Default::default()), + backing_group: GroupIndex(1), + our_assignment: None, + }); + + candidate_info + }; + + + let block_hash_a = Hash::repeat_byte(1); // 1 + let block_hash_b = Hash::repeat_byte(2); + let block_hash_c = Hash::repeat_byte(3); + let block_hash_d = Hash::repeat_byte(4); // 4 + + let block_entry_a = make_block_entry(block_hash_a, Default::default(), 1, single_candidate_vec.clone()); + let block_entry_b = make_block_entry(block_hash_b, block_hash_a, 2, single_candidate_vec.clone()); + let block_entry_c = make_block_entry(block_hash_c, block_hash_b, 3, single_candidate_vec.clone()); + let block_entry_d = make_block_entry(block_hash_d, block_hash_c, 4, single_candidate_vec.clone()); + + let blocks = vec![ + block_entry_a.clone(), + block_entry_b.clone(), + block_entry_c.clone(), + block_entry_d.clone(), + ]; + + for block_entry in blocks { + add_block_entry( + &store, + block_entry, + n_validators, + |h| candidate_info.get(h).map(|x| x.clone()), + ).unwrap(); + } + + force_approve(&store, block_hash_d, 2).unwrap(); + + assert!(load_block_entry(&store, &block_hash_a).unwrap().unwrap().approved_bitfield.all()); + assert!(load_block_entry(&store, &block_hash_b).unwrap().unwrap().approved_bitfield.all()); + assert!(load_block_entry(&store, &block_hash_c).unwrap().unwrap().approved_bitfield.not_any()); + assert!(load_block_entry(&store, &block_hash_d).unwrap().unwrap().approved_bitfield.not_any()); +} diff --git a/polkadot/node/core/approval-voting/src/import.rs b/polkadot/node/core/approval-voting/src/import.rs index 30deef5914..d56b115988 100644 --- a/polkadot/node/core/approval-voting/src/import.rs +++ b/polkadot/node/core/approval-voting/src/import.rs @@ -36,7 +36,7 @@ use polkadot_node_subsystem::{ }; use polkadot_primitives::v1::{ Hash, SessionIndex, SessionInfo, CandidateEvent, Header, CandidateHash, - CandidateReceipt, CoreIndex, GroupIndex, BlockNumber, + CandidateReceipt, CoreIndex, GroupIndex, BlockNumber, ConsensusLog, }; use polkadot_node_primitives::approval::{ self as approval_types, BlockApprovalMeta, RelayVRFStory, @@ -345,6 +345,7 @@ struct ImportedBlockInfo { n_validators: usize, relay_vrf_story: RelayVRFStory, slot: Slot, + force_approve: Option, } struct ImportedBlockInfoEnv<'a> { @@ -494,6 +495,23 @@ async fn imported_block_info( } }; + let force_approve = + block_header.digest.convert_first(|l| match ConsensusLog::from_digest_item(l) { + Ok(Some(ConsensusLog::ForceApprove(num))) if num < block_header.number => Some(num), + Ok(Some(_)) => None, + Ok(None) => None, + Err(err) => { + tracing::warn!( + target: LOG_TARGET, + ?err, + ?block_hash, + "Malformed consensus digest in header", + ); + + None + } + }); + Ok(Some(ImportedBlockInfo { included_candidates, session_index, @@ -501,6 +519,7 @@ async fn imported_block_info( n_validators: session_info.validators.len(), relay_vrf_story, slot, + force_approve, })) } @@ -624,6 +643,7 @@ pub(crate) async fn handle_new_head( n_validators, relay_vrf_story, slot, + force_approve, } = imported_block_info; let session_info = state.session_window.session_info(session_index) @@ -676,6 +696,8 @@ pub(crate) async fn handle_new_head( let block_entry = approval_db::v1::BlockEntry { block_hash, + parent_hash: block_header.parent_hash, + block_number: block_header.number, session: session_index, slot, relay_vrf_story: relay_vrf_story.0, @@ -685,10 +707,13 @@ pub(crate) async fn handle_new_head( children: Vec::new(), }; + if let Some(up_to) = force_approve { + approval_db::v1::force_approve(db_writer, block_hash, up_to) + .map_err(|e| SubsystemError::with_origin("approval-voting", e))?; + } + let candidate_entries = approval_db::v1::add_block_entry( db_writer, - block_header.parent_hash, - block_header.number, block_entry, n_validators, |candidate_hash| { @@ -1026,6 +1051,8 @@ mod tests { known_hash, crate::approval_db::v1::BlockEntry { block_hash: known_hash, + parent_hash: Default::default(), + block_number: known_number, session: 1, slot: Slot::from(100), relay_vrf_story: Default::default(), @@ -1101,6 +1128,8 @@ mod tests { head_hash, crate::approval_db::v1::BlockEntry { block_hash: head_hash, + parent_hash: Default::default(), + block_number: 18, session: 1, slot: Slot::from(100), relay_vrf_story: Default::default(), @@ -1149,6 +1178,8 @@ mod tests { parent_hash, crate::approval_db::v1::BlockEntry { block_hash: parent_hash, + parent_hash: Default::default(), + block_number: 18, session: 1, slot: Slot::from(100), relay_vrf_story: Default::default(), @@ -1195,6 +1226,8 @@ mod tests { parent_hash, crate::approval_db::v1::BlockEntry { block_hash: parent_hash, + parent_hash: Default::default(), + block_number: 18, session: 1, slot: Slot::from(100), relay_vrf_story: Default::default(), @@ -1346,6 +1379,7 @@ mod tests { assert!(info.assignments.is_empty()); assert_eq!(info.n_validators, 0); assert_eq!(info.slot, slot); + assert!(info.force_approve.is_none()); }) }; @@ -1586,6 +1620,142 @@ mod tests { futures::executor::block_on(futures::future::join(test_fut, aux_fut)); } + #[test] + fn imported_block_info_extracts_force_approve() { + let pool = TaskExecutor::new(); + let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone()); + + let session = 5; + let session_info = dummy_session_info(session); + + let slot = Slot::from(10); + + let header = Header { + digest: { + let mut d = Digest::default(); + let (vrf_output, vrf_proof) = garbage_vrf(); + d.push(DigestItem::babe_pre_digest(PreDigest::SecondaryVRF( + SecondaryVRFPreDigest { + authority_index: 0, + slot, + vrf_output, + vrf_proof, + } + ))); + + d.push(ConsensusLog::ForceApprove(3).into()); + + d + }, + extrinsics_root: Default::default(), + number: 5, + state_root: Default::default(), + parent_hash: Default::default(), + }; + + let hash = header.hash(); + let make_candidate = |para_id| { + let mut r = CandidateReceipt::default(); + r.descriptor.para_id = para_id; + r.descriptor.relay_parent = hash; + r + }; + let candidates = vec![ + (make_candidate(1.into()), CoreIndex(0), GroupIndex(2)), + (make_candidate(2.into()), CoreIndex(1), GroupIndex(3)), + ]; + + + let inclusion_events = candidates.iter().cloned() + .map(|(r, c, g)| CandidateEvent::CandidateIncluded(r, Vec::new().into(), c, g)) + .collect::>(); + + let test_fut = { + let included_candidates = candidates.iter() + .map(|(r, c, g)| (r.hash(), r.clone(), *c, *g)) + .collect::>(); + + let session_window = { + let mut window = RollingSessionWindow::default(); + + window.earliest_session = Some(session); + window.session_info.push(session_info); + + window + }; + + let header = header.clone(); + Box::pin(async move { + let env = ImportedBlockInfoEnv { + session_window: &session_window, + assignment_criteria: &MockAssignmentCriteria, + keystore: &LocalKeystore::in_memory(), + }; + + let info = imported_block_info( + &mut ctx, + env, + hash, + &header, + ).await.unwrap().unwrap(); + + assert_eq!(info.included_candidates, included_candidates); + assert_eq!(info.session_index, session); + assert!(info.assignments.is_empty()); + assert_eq!(info.n_validators, 0); + assert_eq!(info.slot, slot); + assert_eq!(info.force_approve, Some(3)); + }) + }; + + let aux_fut = Box::pin(async move { + assert_matches!( + handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + h, + RuntimeApiRequest::CandidateEvents(c_tx), + )) => { + assert_eq!(h, hash); + let _ = c_tx.send(Ok(inclusion_events)); + } + ); + + assert_matches!( + handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + h, + RuntimeApiRequest::SessionIndexForChild(c_tx), + )) => { + assert_eq!(h, header.parent_hash); + let _ = c_tx.send(Ok(session)); + } + ); + + assert_matches!( + handle.recv().await, + AllMessages::RuntimeApi(RuntimeApiMessage::Request( + h, + RuntimeApiRequest::CurrentBabeEpoch(c_tx), + )) => { + assert_eq!(h, hash); + let _ = c_tx.send(Ok(BabeEpoch { + epoch_index: session as _, + start_slot: Slot::from(0), + duration: 200, + authorities: vec![(Sr25519Keyring::Alice.public().into(), 1)], + randomness: [0u8; 32], + config: BabeEpochConfiguration { + c: (1, 4), + allowed_slots: AllowedSlots::PrimarySlots, + }, + })); + } + ); + }); + + futures::executor::block_on(futures::future::join(test_fut, aux_fut)); + } + #[test] fn insta_approval_works() { let pool = TaskExecutor::new(); @@ -1652,6 +1822,8 @@ mod tests { parent_hash.clone(), crate::approval_db::v1::BlockEntry { block_hash: parent_hash.clone(), + parent_hash: Default::default(), + block_number: 4, session, slot, relay_vrf_story: Default::default(), diff --git a/polkadot/node/core/approval-voting/src/persisted_entries.rs b/polkadot/node/core/approval-voting/src/persisted_entries.rs index 441c676bb9..855e1e8b29 100644 --- a/polkadot/node/core/approval-voting/src/persisted_entries.rs +++ b/polkadot/node/core/approval-voting/src/persisted_entries.rs @@ -23,7 +23,7 @@ use polkadot_node_primitives::approval::{DelayTranche, RelayVRFStory, AssignmentCert}; use polkadot_primitives::v1::{ ValidatorIndex, CandidateReceipt, SessionIndex, GroupIndex, CoreIndex, - Hash, CandidateHash, + Hash, CandidateHash, BlockNumber, }; use sp_consensus_slots::Slot; @@ -302,6 +302,8 @@ impl From for crate::approval_db::v1::CandidateEntry { #[derive(Debug, Clone, PartialEq)] pub struct BlockEntry { block_hash: Hash, + parent_hash: Hash, + block_number: BlockNumber, session: SessionIndex, slot: Slot, relay_vrf_story: RelayVRFStory, @@ -401,6 +403,8 @@ impl From for BlockEntry { fn from(entry: crate::approval_db::v1::BlockEntry) -> Self { BlockEntry { block_hash: entry.block_hash, + parent_hash: entry.parent_hash, + block_number: entry.block_number, session: entry.session, slot: entry.slot, relay_vrf_story: RelayVRFStory(entry.relay_vrf_story), @@ -415,6 +419,8 @@ impl From for crate::approval_db::v1::BlockEntry { fn from(entry: BlockEntry) -> Self { Self { block_hash: entry.block_hash, + parent_hash: entry.parent_hash, + block_number: entry.block_number, session: entry.session, slot: entry.slot, relay_vrf_story: entry.relay_vrf_story.0, diff --git a/polkadot/node/core/approval-voting/src/tests.rs b/polkadot/node/core/approval-voting/src/tests.rs index 9b9c153919..c60dbbcb32 100644 --- a/polkadot/node/core/approval-voting/src/tests.rs +++ b/polkadot/node/core/approval-voting/src/tests.rs @@ -314,6 +314,8 @@ fn add_block( block_hash, approval_db::v1::BlockEntry { block_hash, + parent_hash: Default::default(), + block_number: 0, session, slot, candidates: Vec::new(), diff --git a/polkadot/primitives/src/v1.rs b/polkadot/primitives/src/v1.rs index 9501cf6d2e..94c1bff516 100644 --- a/polkadot/primitives/src/v1.rs +++ b/polkadot/primitives/src/v1.rs @@ -1064,6 +1064,43 @@ pub struct AbridgedHrmpChannel { pub mqc_head: Option, } +/// Consensus engine id for polkadot v1 consensus engine. +pub const POLKADOT_ENGINE_ID: runtime_primitives::ConsensusEngineId = *b"POL1"; + +/// A consensus log item for polkadot validation. To be used with [`POLKADOT_ENGINE_ID`]. +#[derive(Decode, Encode, Clone, PartialEq, Eq)] +pub enum ConsensusLog { + /// A parachain or parathread upgraded its code. + #[codec(index = 1)] + ParaUpgradeCode(Id, Hash), + /// A parachain or parathread scheduled a code ugprade. + #[codec(index = 2)] + ParaScheduleUpgradeCode(Id, Hash, BlockNumber), + /// Governance requests to auto-approve every candidate included up to the given block + /// number in the current chain, inclusive. + #[codec(index = 3)] + ForceApprove(BlockNumber), +} + +impl ConsensusLog { + /// Attempt to convert a reference to a generic digest item into a consensus log. + pub fn from_digest_item(digest_item: &runtime_primitives::DigestItem) + -> Result, parity_scale_codec::Error> + { + match digest_item { + runtime_primitives::DigestItem::Consensus(id, encoded) if id == &POLKADOT_ENGINE_ID => + Ok(Some(Self::decode(&mut &encoded[..])?)), + _ => Ok(None), + } + } +} + +impl From for runtime_primitives::DigestItem { + fn from(c: ConsensusLog) -> runtime_primitives::DigestItem { + Self::Consensus(POLKADOT_ENGINE_ID, c.encode()) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/polkadot/runtime/parachains/src/initializer.rs b/polkadot/runtime/parachains/src/initializer.rs index be2ecd3d24..9eb441fd8c 100644 --- a/polkadot/runtime/parachains/src/initializer.rs +++ b/polkadot/runtime/parachains/src/initializer.rs @@ -20,8 +20,9 @@ //! This module can throw fatal errors if session-change notifications are received after initialization. use sp_std::prelude::*; -use frame_support::weights::Weight; -use primitives::v1::{ValidatorId, SessionIndex}; +use frame_support::weights::{Weight, DispatchClass}; +use frame_support::traits::EnsureOrigin; +use primitives::v1::{ValidatorId, SessionIndex, ConsensusLog, BlockNumber}; use frame_support::{ decl_storage, decl_module, decl_error, traits::{OneSessionHandler, Randomness}, }; @@ -82,6 +83,8 @@ pub trait Config: { /// A randomness beacon. type Randomness: Randomness; + /// An origin which is allowed to force updates to parachains. + type ForceOrigin: EnsureOrigin<::Origin>; } decl_storage! { @@ -168,6 +171,16 @@ decl_module! { HasInitialized::take(); } + + /// Issue a signal to the consensus engine to forcibly act as though all parachain + /// blocks in all relay chain blocks up to and including the given number in the current + /// chain are valid and should be finalized. + #[weight = (0, DispatchClass::Operational)] + fn force_approve(origin, up_to: BlockNumber) { + T::ForceOrigin::ensure_origin(origin)?; + + frame_system::Pallet::::deposit_log(ConsensusLog::ForceApprove(up_to).into()); + } } } diff --git a/polkadot/runtime/parachains/src/mock.rs b/polkadot/runtime/parachains/src/mock.rs index 7861b81e85..bd69a9b015 100644 --- a/polkadot/runtime/parachains/src/mock.rs +++ b/polkadot/runtime/parachains/src/mock.rs @@ -102,6 +102,7 @@ impl pallet_balances::Config for Test { impl crate::initializer::Config for Test { type Randomness = TestRandomness; + type ForceOrigin = frame_system::EnsureRoot; } impl crate::configuration::Config for Test { } diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index 14c988b552..a664293248 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -589,6 +589,7 @@ impl parachains_scheduler::Config for Runtime {} impl parachains_initializer::Config for Runtime { type Randomness = pallet_babe::RandomnessFromOneEpochAgo; + type ForceOrigin = EnsureRoot; } impl paras_sudo_wrapper::Config for Runtime {} diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index f8a229d03f..81117580ea 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -459,6 +459,7 @@ impl parachains_inclusion_inherent::Config for Runtime {} impl parachains_initializer::Config for Runtime { type Randomness = pallet_babe::RandomnessFromOneEpochAgo; + type ForceOrigin = frame_system::EnsureRoot; } impl parachains_session_info::Config for Runtime {}