Unverified Commit 2195fb5a authored by asynchronous rob's avatar asynchronous rob Committed by GitHub
Browse files

Approval voting failsafe (#2675)



* add consensus log type

* origin and issue force_approve

* add origin in runtimes

* ref API

* scrape force_approve digest from header

* add parent_hash to BlockEntry

* add block_number to block entry and force_approve skeleton

* implement and plug in force-approve

* test force_approve

* test force_approve extraction

* westend runtime

* Update node/core/approval-voting/src/approval_db/v1/mod.rs

Co-authored-by: default avatarBastian Köcher <bkchr@users.noreply.github.com>

* rename

* Update runtime/parachains/src/initializer.rs

Co-authored-by: default avatarAndré Silva <123550+andresilva@users.noreply.github.com>

Co-authored-by: default avatarBastian Köcher <bkchr@users.noreply.github.com>
Co-authored-by: default avatarAndré Silva <123550+andresilva@users.noreply.github.com>
parent 709e8723
Pipeline #131018 failed with stages
in 26 minutes and 17 seconds
......@@ -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<NewCandidateInfo>,
) -> Result<Vec<(CandidateHash, CandidateEntry)>> {
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"]
......
......@@ -57,10 +57,14 @@ fn make_bitvec(len: usize) -> BitVec<BitOrderLsb0, u8> {
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());
}
......@@ -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<BlockNumber>,
}
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::<Vec<_>>();
let test_fut = {
let included_candidates = candidates.iter()
.map(|(r, c, g)| (r.hash(), r.clone(), *c, *g))
.collect::<Vec<_>>();
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(),
......
......@@ -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<CandidateEntry> 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<crate::approval_db::v1::BlockEntry> 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<BlockEntry> 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,