Approve multiple candidates with a single signature (#1191)

Initial implementation for the plan discussed here: https://github.com/paritytech/polkadot-sdk/issues/701
Built on top of https://github.com/paritytech/polkadot-sdk/pull/1178
v0: https://github.com/paritytech/polkadot/pull/7554,

## Overall idea

When approval-voting checks a candidate and is ready to advertise the
approval, defer it in a per-relay chain block until we either have
MAX_APPROVAL_COALESCE_COUNT candidates to sign or a candidate has stayed
MAX_APPROVALS_COALESCE_TICKS in the queue, in both cases we sign what
candidates we have available.

This should allow us to reduce the number of approvals messages we have
to create/send/verify. The parameters are configurable, so we should
find some values that balance:

- Security of the network: Delaying broadcasting of an approval
shouldn't but the finality at risk and to make sure that never happens
we won't delay sending a vote if we are past 2/3 from the no-show time.
- Scalability of the network: MAX_APPROVAL_COALESCE_COUNT = 1 &
MAX_APPROVALS_COALESCE_TICKS =0, is what we have now and we know from
the measurements we did on versi, it bottlenecks
approval-distribution/approval-voting when increase significantly the
number of validators and parachains
- Block storage: In case of disputes we have to import this votes on
chain and that increase the necessary storage with
MAX_APPROVAL_COALESCE_COUNT * CandidateHash per vote. Given that
disputes are not the normal way of the network functioning and we will
limit MAX_APPROVAL_COALESCE_COUNT in the single digits numbers, this
should be good enough. Alternatively, we could try to create a better
way to store this on-chain through indirection, if that's needed.

## Other fixes:
- Fixed the fact that we were sending random assignments to
non-validators, that was wrong because those won't do anything with it
and they won't gossip it either because they do not have a grid topology
set, so we would waste the random assignments.
- Added metrics to be able to debug potential no-shows and
mis-processing of approvals/assignments.

## TODO:
- [x] Get feedback, that this is moving in the right direction. @ordian
@sandreim @eskimor @burdges, let me know what you think.
- [x] More and more testing.
- [x]  Test in versi.
- [x] Make MAX_APPROVAL_COALESCE_COUNT &
MAX_APPROVAL_COALESCE_WAIT_MILLIS a parachain host configuration.
- [x] Make sure the backwards compatibility works correctly
- [x] Make sure this direction is compatible with other streams of work:
https://github.com/paritytech/polkadot-sdk/issues/635 &
https://github.com/paritytech/polkadot-sdk/issues/742
- [x] Final versi burn-in before merging

---------

Signed-off-by: Alexandru Gheorghe <alexandru.gheorghe@parity.io>
This commit is contained in:
Alexandru Gheorghe
2023-12-13 08:43:15 +02:00
committed by GitHub
parent d18a682bf7
commit a84dd0dba5
82 changed files with 5883 additions and 1483 deletions
@@ -25,6 +25,15 @@ use crate::{
time::Tick,
};
/// Result of counting the necessary tranches needed for approving a block.
#[derive(Debug, PartialEq, Clone)]
pub struct TranchesToApproveResult {
/// The required tranches for approving this block
pub required_tranches: RequiredTranches,
/// The total number of no_shows at the moment we are doing the counting.
pub total_observed_no_shows: usize,
}
/// The required tranches of assignments needed to determine whether a candidate is approved.
#[derive(Debug, PartialEq, Clone)]
pub enum RequiredTranches {
@@ -64,7 +73,7 @@ pub enum RequiredTranches {
}
/// The result of a check.
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Check {
/// The candidate is unapproved.
Unapproved,
@@ -178,6 +187,7 @@ struct State {
next_no_show: Option<Tick>,
/// The last tick at which a considered assignment was received.
last_assignment_tick: Option<Tick>,
total_observed_no_shows: usize,
}
impl State {
@@ -187,41 +197,53 @@ impl State {
needed_approvals: usize,
n_validators: usize,
no_show_duration: Tick,
) -> RequiredTranches {
) -> TranchesToApproveResult {
let covering = if self.depth == 0 { 0 } else { self.covering };
if self.depth != 0 && self.assignments + covering + self.uncovered >= n_validators {
return RequiredTranches::All
return TranchesToApproveResult {
required_tranches: RequiredTranches::All,
total_observed_no_shows: self.total_observed_no_shows,
}
}
// If we have enough assignments and all no-shows are covered, we have reached the number
// of tranches that we need to have.
if self.assignments >= needed_approvals && (covering + self.uncovered) == 0 {
return RequiredTranches::Exact {
needed: tranche,
tolerated_missing: self.covered,
next_no_show: self.next_no_show,
last_assignment_tick: self.last_assignment_tick,
return TranchesToApproveResult {
required_tranches: RequiredTranches::Exact {
needed: tranche,
tolerated_missing: self.covered,
next_no_show: self.next_no_show,
last_assignment_tick: self.last_assignment_tick,
},
total_observed_no_shows: self.total_observed_no_shows,
}
}
// We're pending more assignments and should look at more tranches.
let clock_drift = self.clock_drift(no_show_duration);
if self.depth == 0 {
RequiredTranches::Pending {
considered: tranche,
next_no_show: self.next_no_show,
// during the initial assignment-gathering phase, we want to accept assignments
// from any tranche. Note that honest validators will still not broadcast their
// assignment until it is time to do so, regardless of this value.
maximum_broadcast: DelayTranche::max_value(),
clock_drift,
TranchesToApproveResult {
required_tranches: RequiredTranches::Pending {
considered: tranche,
next_no_show: self.next_no_show,
// during the initial assignment-gathering phase, we want to accept assignments
// from any tranche. Note that honest validators will still not broadcast their
// assignment until it is time to do so, regardless of this value.
maximum_broadcast: DelayTranche::max_value(),
clock_drift,
},
total_observed_no_shows: self.total_observed_no_shows,
}
} else {
RequiredTranches::Pending {
considered: tranche,
next_no_show: self.next_no_show,
maximum_broadcast: tranche + (covering + self.uncovered) as DelayTranche,
clock_drift,
TranchesToApproveResult {
required_tranches: RequiredTranches::Pending {
considered: tranche,
next_no_show: self.next_no_show,
maximum_broadcast: tranche + (covering + self.uncovered) as DelayTranche,
clock_drift,
},
total_observed_no_shows: self.total_observed_no_shows,
}
}
}
@@ -276,6 +298,7 @@ impl State {
uncovered,
next_no_show,
last_assignment_tick,
total_observed_no_shows: self.total_observed_no_shows + new_no_shows,
}
}
}
@@ -372,7 +395,7 @@ pub fn tranches_to_approve(
block_tick: Tick,
no_show_duration: Tick,
needed_approvals: usize,
) -> RequiredTranches {
) -> TranchesToApproveResult {
let tick_now = tranche_now as Tick + block_tick;
let n_validators = approval_entry.n_validators();
@@ -384,6 +407,7 @@ pub fn tranches_to_approve(
uncovered: 0,
next_no_show: None,
last_assignment_tick: None,
total_observed_no_shows: 0,
};
// The `ApprovalEntry` doesn't have any data for empty tranches. We still want to iterate over
@@ -434,7 +458,7 @@ pub fn tranches_to_approve(
let s = s.advance(n_assignments, no_shows, next_no_show, last_assignment_tick);
let output = s.output(tranche, needed_approvals, n_validators, no_show_duration);
*state = match output {
*state = match output.required_tranches {
RequiredTranches::Exact { .. } | RequiredTranches::All => {
// Wipe the state clean so the next iteration of this closure will terminate
// the iterator. This guarantees that we can call `last` further down to see
@@ -464,15 +488,17 @@ mod tests {
#[test]
fn pending_is_not_approved() {
let candidate = approval_db::v1::CandidateEntry {
candidate: dummy_candidate_receipt(dummy_hash()),
session: 0,
block_assignments: BTreeMap::default(),
approvals: BitVec::default(),
}
.into();
let candidate = CandidateEntry::from_v1(
approval_db::v1::CandidateEntry {
candidate: dummy_candidate_receipt(dummy_hash()),
session: 0,
block_assignments: BTreeMap::default(),
approvals: BitVec::default(),
},
0,
);
let approval_entry = approval_db::v2::ApprovalEntry {
let approval_entry = approval_db::v3::ApprovalEntry {
tranches: Vec::new(),
assigned_validators: BitVec::default(),
our_assignment: None,
@@ -497,29 +523,31 @@ mod tests {
#[test]
fn exact_takes_only_assignments_up_to() {
let mut candidate: CandidateEntry = approval_db::v1::CandidateEntry {
candidate: dummy_candidate_receipt(dummy_hash()),
session: 0,
block_assignments: BTreeMap::default(),
approvals: bitvec![u8, BitOrderLsb0; 0; 10],
}
.into();
let mut candidate: CandidateEntry = CandidateEntry::from_v1(
approval_db::v1::CandidateEntry {
candidate: dummy_candidate_receipt(dummy_hash()),
session: 0,
block_assignments: BTreeMap::default(),
approvals: bitvec![u8, BitOrderLsb0; 0; 10],
},
0,
);
for i in 0..3 {
candidate.mark_approval(ValidatorIndex(i));
}
let approval_entry = approval_db::v2::ApprovalEntry {
let approval_entry = approval_db::v3::ApprovalEntry {
tranches: vec![
approval_db::v2::TrancheEntry {
approval_db::v3::TrancheEntry {
tranche: 0,
assignments: (0..2).map(|i| (ValidatorIndex(i), 0.into())).collect(),
},
approval_db::v2::TrancheEntry {
approval_db::v3::TrancheEntry {
tranche: 1,
assignments: (2..5).map(|i| (ValidatorIndex(i), 1.into())).collect(),
},
approval_db::v2::TrancheEntry {
approval_db::v3::TrancheEntry {
tranche: 2,
assignments: (5..10).map(|i| (ValidatorIndex(i), 0.into())).collect(),
},
@@ -569,29 +597,31 @@ mod tests {
#[test]
fn one_honest_node_always_approves() {
let mut candidate: CandidateEntry = approval_db::v1::CandidateEntry {
candidate: dummy_candidate_receipt(dummy_hash()),
session: 0,
block_assignments: BTreeMap::default(),
approvals: bitvec![u8, BitOrderLsb0; 0; 10],
}
.into();
let mut candidate: CandidateEntry = CandidateEntry::from_v1(
approval_db::v1::CandidateEntry {
candidate: dummy_candidate_receipt(dummy_hash()),
session: 0,
block_assignments: BTreeMap::default(),
approvals: bitvec![u8, BitOrderLsb0; 0; 10],
},
0,
);
for i in 0..3 {
candidate.mark_approval(ValidatorIndex(i));
}
let approval_entry = approval_db::v2::ApprovalEntry {
let approval_entry = approval_db::v3::ApprovalEntry {
tranches: vec![
approval_db::v2::TrancheEntry {
approval_db::v3::TrancheEntry {
tranche: 0,
assignments: (0..4).map(|i| (ValidatorIndex(i), 0.into())).collect(),
},
approval_db::v2::TrancheEntry {
approval_db::v3::TrancheEntry {
tranche: 1,
assignments: (4..6).map(|i| (ValidatorIndex(i), 1.into())).collect(),
},
approval_db::v2::TrancheEntry {
approval_db::v3::TrancheEntry {
tranche: 2,
assignments: (6..10).map(|i| (ValidatorIndex(i), 0.into())).collect(),
},
@@ -647,7 +677,7 @@ mod tests {
let no_show_duration = 10;
let needed_approvals = 4;
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v3::ApprovalEntry {
tranches: Vec::new(),
assigned_validators: bitvec![u8, BitOrderLsb0; 0; 5],
our_assignment: None,
@@ -675,7 +705,8 @@ mod tests {
block_tick,
no_show_duration,
needed_approvals,
),
)
.required_tranches,
RequiredTranches::Exact {
needed: 1,
tolerated_missing: 0,
@@ -691,7 +722,7 @@ mod tests {
let no_show_duration = 10;
let needed_approvals = 4;
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v3::ApprovalEntry {
tranches: Vec::new(),
assigned_validators: bitvec![u8, BitOrderLsb0; 0; 10],
our_assignment: None,
@@ -715,7 +746,8 @@ mod tests {
block_tick,
no_show_duration,
needed_approvals,
),
)
.required_tranches,
RequiredTranches::Pending {
considered: 2,
next_no_show: Some(block_tick + no_show_duration),
@@ -731,7 +763,7 @@ mod tests {
let no_show_duration = 10;
let needed_approvals = 4;
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v3::ApprovalEntry {
tranches: Vec::new(),
assigned_validators: bitvec![u8, BitOrderLsb0; 0; 10],
our_assignment: None,
@@ -759,7 +791,8 @@ mod tests {
block_tick,
no_show_duration,
needed_approvals,
),
)
.required_tranches,
RequiredTranches::Pending {
considered: 11,
next_no_show: None,
@@ -776,7 +809,7 @@ mod tests {
let needed_approvals = 4;
let n_validators = 8;
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v3::ApprovalEntry {
tranches: Vec::new(),
assigned_validators: bitvec![u8, BitOrderLsb0; 0; n_validators],
our_assignment: None,
@@ -807,7 +840,8 @@ mod tests {
block_tick,
no_show_duration,
needed_approvals,
),
)
.required_tranches,
RequiredTranches::Pending {
considered: 1,
next_no_show: None,
@@ -826,7 +860,8 @@ mod tests {
block_tick,
no_show_duration,
needed_approvals,
),
)
.required_tranches,
RequiredTranches::Pending {
considered: 1,
next_no_show: None,
@@ -843,7 +878,7 @@ mod tests {
let needed_approvals = 4;
let n_validators = 8;
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v3::ApprovalEntry {
tranches: Vec::new(),
assigned_validators: bitvec![u8, BitOrderLsb0; 0; n_validators],
our_assignment: None,
@@ -879,7 +914,8 @@ mod tests {
block_tick,
no_show_duration,
needed_approvals,
),
)
.required_tranches,
RequiredTranches::Exact {
needed: 1,
tolerated_missing: 0,
@@ -898,7 +934,8 @@ mod tests {
block_tick,
no_show_duration,
needed_approvals,
),
)
.required_tranches,
RequiredTranches::Exact {
needed: 2,
tolerated_missing: 1,
@@ -917,7 +954,8 @@ mod tests {
block_tick,
no_show_duration,
needed_approvals,
),
)
.required_tranches,
RequiredTranches::Pending {
considered: 2,
next_no_show: None,
@@ -934,7 +972,7 @@ mod tests {
let needed_approvals = 4;
let n_validators = 8;
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v3::ApprovalEntry {
tranches: Vec::new(),
assigned_validators: bitvec![u8, BitOrderLsb0; 0; n_validators],
our_assignment: None,
@@ -970,7 +1008,8 @@ mod tests {
block_tick,
no_show_duration,
needed_approvals,
),
)
.required_tranches,
RequiredTranches::Exact {
needed: 2,
tolerated_missing: 1,
@@ -992,7 +1031,8 @@ mod tests {
block_tick,
no_show_duration,
needed_approvals,
),
)
.required_tranches,
RequiredTranches::Pending {
considered: 2,
next_no_show: None,
@@ -1013,7 +1053,8 @@ mod tests {
block_tick,
no_show_duration,
needed_approvals,
),
)
.required_tranches,
RequiredTranches::Exact {
needed: 3,
tolerated_missing: 2,
@@ -1029,22 +1070,24 @@ mod tests {
let no_show_duration = 10;
let needed_approvals = 3;
let mut candidate: CandidateEntry = approval_db::v1::CandidateEntry {
candidate: dummy_candidate_receipt(dummy_hash()),
session: 0,
block_assignments: BTreeMap::default(),
approvals: bitvec![u8, BitOrderLsb0; 0; 3],
}
.into();
let mut candidate: CandidateEntry = CandidateEntry::from_v1(
approval_db::v1::CandidateEntry {
candidate: dummy_candidate_receipt(dummy_hash()),
session: 0,
block_assignments: BTreeMap::default(),
approvals: bitvec![u8, BitOrderLsb0; 0; 3],
},
0,
);
for i in 0..3 {
candidate.mark_approval(ValidatorIndex(i));
}
let approval_entry = approval_db::v2::ApprovalEntry {
let approval_entry = approval_db::v3::ApprovalEntry {
tranches: vec![
// Assignments with invalid validator indexes.
approval_db::v2::TrancheEntry {
approval_db::v3::TrancheEntry {
tranche: 1,
assignments: (2..5).map(|i| (ValidatorIndex(i), 1.into())).collect(),
},
@@ -1068,7 +1111,8 @@ mod tests {
block_tick,
no_show_duration,
needed_approvals,
),
)
.required_tranches,
RequiredTranches::Pending {
considered: 10,
next_no_show: None,
@@ -1094,7 +1138,7 @@ mod tests {
];
for test_tranche in test_tranches {
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v3::ApprovalEntry {
tranches: Vec::new(),
backing_group: GroupIndex(0),
our_assignment: None,
@@ -1345,10 +1389,11 @@ mod tests {
uncovered: 0,
next_no_show: None,
last_assignment_tick: None,
total_observed_no_shows: 0,
};
assert_eq!(
state.output(0, 10, 10, 20),
state.output(0, 10, 10, 20).required_tranches,
RequiredTranches::Pending {
considered: 0,
next_no_show: None,
@@ -1368,10 +1413,11 @@ mod tests {
uncovered: 0,
next_no_show: None,
last_assignment_tick: None,
total_observed_no_shows: 0,
};
assert_eq!(
state.output(0, 10, 10, 20),
state.output(0, 10, 10, 20).required_tranches,
RequiredTranches::Exact {
needed: 0,
tolerated_missing: 0,
@@ -0,0 +1,39 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec};
use polkadot_node_primitives::approval::{
v1::{AssignmentCert, AssignmentCertKind, VrfProof, VrfSignature, RELAY_VRF_MODULO_CONTEXT},
v2::VrfPreOutput,
};
pub fn make_bitvec(len: usize) -> BitVec<u8, BitOrderLsb0> {
bitvec::bitvec![u8, BitOrderLsb0; 0; len]
}
pub fn dummy_assignment_cert(kind: AssignmentCertKind) -> AssignmentCert {
let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT);
let msg = b"test-garbage";
let mut prng = rand_core::OsRng;
let keypair = schnorrkel::Keypair::generate_with(&mut prng);
let (inout, proof, _) = keypair.vrf_sign(ctx.bytes(msg));
let out = inout.to_output();
AssignmentCert {
kind,
vrf: VrfSignature { pre_output: VrfPreOutput(out), proof: VrfProof(proof) },
}
}
@@ -0,0 +1,293 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Common helper functions for all versions of approval-voting database.
use std::sync::Arc;
use parity_scale_codec::{Decode, Encode};
use polkadot_node_subsystem::{SubsystemError, SubsystemResult};
use polkadot_node_subsystem_util::database::{DBTransaction, Database};
use polkadot_primitives::{BlockNumber, CandidateHash, CandidateIndex, Hash};
use crate::{
backend::{Backend, BackendWriteOp, V1ReadBackend, V2ReadBackend},
persisted_entries,
};
use super::{
v2::{load_block_entry_v1, load_candidate_entry_v1},
v3::{load_block_entry_v2, load_candidate_entry_v2, BlockEntry, CandidateEntry},
};
pub mod migration_helpers;
const STORED_BLOCKS_KEY: &[u8] = b"Approvals_StoredBlocks";
/// A range from earliest..last block number stored within the DB.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct StoredBlockRange(pub BlockNumber, pub BlockNumber);
/// The database config.
#[derive(Debug, Clone, Copy)]
pub struct Config {
/// The column family in the database where data is stored.
pub col_approval_data: u32,
}
/// `DbBackend` is a concrete implementation of the higher-level Backend trait
pub struct DbBackend {
inner: Arc<dyn Database>,
config: Config,
}
impl DbBackend {
/// Create a new [`DbBackend`] with the supplied key-value store and
/// config.
pub fn new(db: Arc<dyn Database>, config: Config) -> Self {
DbBackend { inner: db, config }
}
}
/// Errors while accessing things from the DB.
#[derive(Debug, derive_more::From, derive_more::Display)]
pub enum Error {
Io(std::io::Error),
InvalidDecoding(parity_scale_codec::Error),
InternalError(SubsystemError),
}
impl std::error::Error for Error {}
/// Result alias for DB errors.
pub type Result<T> = std::result::Result<T, Error>;
impl Backend for DbBackend {
fn load_block_entry(
&self,
block_hash: &Hash,
) -> SubsystemResult<Option<persisted_entries::BlockEntry>> {
load_block_entry(&*self.inner, &self.config, block_hash).map(|e| e.map(Into::into))
}
fn load_candidate_entry(
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<persisted_entries::CandidateEntry>> {
load_candidate_entry(&*self.inner, &self.config, candidate_hash).map(|e| e.map(Into::into))
}
fn load_blocks_at_height(&self, block_height: &BlockNumber) -> SubsystemResult<Vec<Hash>> {
load_blocks_at_height(&*self.inner, &self.config, block_height)
}
fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>> {
load_all_blocks(&*self.inner, &self.config)
}
fn load_stored_blocks(&self) -> SubsystemResult<Option<StoredBlockRange>> {
load_stored_blocks(&*self.inner, &self.config)
}
/// Atomically write the list of operations, with later operations taking precedence over prior.
fn write<I>(&mut self, ops: I) -> SubsystemResult<()>
where
I: IntoIterator<Item = BackendWriteOp>,
{
let mut tx = DBTransaction::new();
for op in ops {
match op {
BackendWriteOp::WriteStoredBlockRange(stored_block_range) => {
tx.put_vec(
self.config.col_approval_data,
&STORED_BLOCKS_KEY,
stored_block_range.encode(),
);
},
BackendWriteOp::DeleteStoredBlockRange => {
tx.delete(self.config.col_approval_data, &STORED_BLOCKS_KEY);
},
BackendWriteOp::WriteBlocksAtHeight(h, blocks) => {
tx.put_vec(
self.config.col_approval_data,
&blocks_at_height_key(h),
blocks.encode(),
);
},
BackendWriteOp::DeleteBlocksAtHeight(h) => {
tx.delete(self.config.col_approval_data, &blocks_at_height_key(h));
},
BackendWriteOp::WriteBlockEntry(block_entry) => {
let block_entry: BlockEntry = block_entry.into();
tx.put_vec(
self.config.col_approval_data,
&block_entry_key(&block_entry.block_hash),
block_entry.encode(),
);
},
BackendWriteOp::DeleteBlockEntry(hash) => {
tx.delete(self.config.col_approval_data, &block_entry_key(&hash));
},
BackendWriteOp::WriteCandidateEntry(candidate_entry) => {
let candidate_entry: CandidateEntry = candidate_entry.into();
tx.put_vec(
self.config.col_approval_data,
&candidate_entry_key(&candidate_entry.candidate.hash()),
candidate_entry.encode(),
);
},
BackendWriteOp::DeleteCandidateEntry(candidate_hash) => {
tx.delete(self.config.col_approval_data, &candidate_entry_key(&candidate_hash));
},
}
}
self.inner.write(tx).map_err(|e| e.into())
}
}
impl V1ReadBackend for DbBackend {
fn load_candidate_entry_v1(
&self,
candidate_hash: &CandidateHash,
candidate_index: CandidateIndex,
) -> SubsystemResult<Option<persisted_entries::CandidateEntry>> {
load_candidate_entry_v1(&*self.inner, &self.config, candidate_hash)
.map(|e| e.map(|e| persisted_entries::CandidateEntry::from_v1(e, candidate_index)))
}
fn load_block_entry_v1(
&self,
block_hash: &Hash,
) -> SubsystemResult<Option<persisted_entries::BlockEntry>> {
load_block_entry_v1(&*self.inner, &self.config, block_hash).map(|e| e.map(Into::into))
}
}
impl V2ReadBackend for DbBackend {
fn load_candidate_entry_v2(
&self,
candidate_hash: &CandidateHash,
candidate_index: CandidateIndex,
) -> SubsystemResult<Option<persisted_entries::CandidateEntry>> {
load_candidate_entry_v2(&*self.inner, &self.config, candidate_hash)
.map(|e| e.map(|e| persisted_entries::CandidateEntry::from_v2(e, candidate_index)))
}
fn load_block_entry_v2(
&self,
block_hash: &Hash,
) -> SubsystemResult<Option<persisted_entries::BlockEntry>> {
load_block_entry_v2(&*self.inner, &self.config, block_hash).map(|e| e.map(Into::into))
}
}
pub(crate) fn load_decode<D: Decode>(
store: &dyn Database,
col_approval_data: u32,
key: &[u8],
) -> Result<Option<D>> {
match store.get(col_approval_data, key)? {
None => Ok(None),
Some(raw) => D::decode(&mut &raw[..]).map(Some).map_err(Into::into),
}
}
/// The key a given block entry is stored under.
pub(crate) fn block_entry_key(block_hash: &Hash) -> [u8; 46] {
const BLOCK_ENTRY_PREFIX: [u8; 14] = *b"Approvals_blck";
let mut key = [0u8; 14 + 32];
key[0..14].copy_from_slice(&BLOCK_ENTRY_PREFIX);
key[14..][..32].copy_from_slice(block_hash.as_ref());
key
}
/// The key a given candidate entry is stored under.
pub(crate) fn candidate_entry_key(candidate_hash: &CandidateHash) -> [u8; 46] {
const CANDIDATE_ENTRY_PREFIX: [u8; 14] = *b"Approvals_cand";
let mut key = [0u8; 14 + 32];
key[0..14].copy_from_slice(&CANDIDATE_ENTRY_PREFIX);
key[14..][..32].copy_from_slice(candidate_hash.0.as_ref());
key
}
/// The key a set of block hashes corresponding to a block number is stored under.
pub(crate) fn blocks_at_height_key(block_number: BlockNumber) -> [u8; 16] {
const BLOCKS_AT_HEIGHT_PREFIX: [u8; 12] = *b"Approvals_at";
let mut key = [0u8; 12 + 4];
key[0..12].copy_from_slice(&BLOCKS_AT_HEIGHT_PREFIX);
block_number.using_encoded(|s| key[12..16].copy_from_slice(s));
key
}
/// Return all blocks which have entries in the DB, ascending, by height.
pub fn load_all_blocks(store: &dyn Database, config: &Config) -> SubsystemResult<Vec<Hash>> {
let mut hashes = Vec::new();
if let Some(stored_blocks) = load_stored_blocks(store, config)? {
for height in stored_blocks.0..stored_blocks.1 {
let blocks = load_blocks_at_height(store, config, &height)?;
hashes.extend(blocks);
}
}
Ok(hashes)
}
/// Load the stored-blocks key from the state.
pub fn load_stored_blocks(
store: &dyn Database,
config: &Config,
) -> SubsystemResult<Option<StoredBlockRange>> {
load_decode(store, config.col_approval_data, STORED_BLOCKS_KEY)
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a blocks-at-height entry for a given block number.
pub fn load_blocks_at_height(
store: &dyn Database,
config: &Config,
block_number: &BlockNumber,
) -> SubsystemResult<Vec<Hash>> {
load_decode(store, config.col_approval_data, &blocks_at_height_key(*block_number))
.map(|x| x.unwrap_or_default())
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a block entry from the aux store.
pub fn load_block_entry(
store: &dyn Database,
config: &Config,
block_hash: &Hash,
) -> SubsystemResult<Option<BlockEntry>> {
load_decode(store, config.col_approval_data, &block_entry_key(block_hash))
.map(|u: Option<BlockEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a candidate entry from the aux store in current version format.
pub fn load_candidate_entry(
store: &dyn Database,
config: &Config,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>> {
load_decode(store, config.col_approval_data, &candidate_entry_key(candidate_hash))
.map(|u: Option<CandidateEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
@@ -30,5 +30,7 @@
//! In the future, we may use a temporary DB which doesn't need to be wiped, but for the
//! time being we share the same DB with the rest of Substrate.
pub mod common;
pub mod v1;
pub mod v2;
pub mod v3;
@@ -40,10 +40,6 @@ fn make_db() -> (DbBackend, Arc<dyn Database>) {
(DbBackend::new(db_writer.clone(), TEST_CONFIG), db_writer)
}
fn make_bitvec(len: usize) -> BitVec<u8, BitOrderLsb0> {
bitvec::bitvec![u8, BitOrderLsb0; 0; len]
}
fn make_block_entry(
block_hash: Hash,
parent_hash: Hash,
@@ -16,29 +16,19 @@
//! Approval DB migration helpers.
use super::*;
use crate::backend::Backend;
use polkadot_node_primitives::approval::v1::{
AssignmentCert, AssignmentCertKind, VrfPreOutput, VrfProof, VrfSignature,
RELAY_VRF_MODULO_CONTEXT,
use crate::{
approval_db::common::{
migration_helpers::{dummy_assignment_cert, make_bitvec},
Error, Result, StoredBlockRange,
},
backend::Backend,
};
use polkadot_node_primitives::approval::v1::AssignmentCertKind;
use polkadot_node_subsystem_util::database::Database;
use sp_application_crypto::sp_core::H256;
use std::{collections::HashSet, sync::Arc};
fn dummy_assignment_cert(kind: AssignmentCertKind) -> AssignmentCert {
let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT);
let msg = b"test-garbage";
let mut prng = rand_core::OsRng;
let keypair = schnorrkel::Keypair::generate_with(&mut prng);
let (inout, proof, _) = keypair.vrf_sign(ctx.bytes(msg));
let preout = inout.to_output();
AssignmentCert {
kind,
vrf: VrfSignature { pre_output: VrfPreOutput(preout), proof: VrfProof(proof) },
}
}
fn make_block_entry_v1(
block_hash: Hash,
parent_hash: Hash,
@@ -58,14 +48,10 @@ fn make_block_entry_v1(
}
}
fn make_bitvec(len: usize) -> BitVec<u8, BitOrderLsb0> {
bitvec::bitvec![u8, BitOrderLsb0; 0; len]
}
/// Migrates `OurAssignment`, `CandidateEntry` and `ApprovalEntry` to version 2.
/// Returns on any error.
/// Must only be used in parachains DB migration code - `polkadot-service` crate.
pub fn v1_to_v2(db: Arc<dyn Database>, config: Config) -> Result<()> {
pub fn v1_to_latest(db: Arc<dyn Database>, config: Config) -> Result<()> {
let mut backend = crate::DbBackend::new(db, config);
let all_blocks = backend
.load_all_blocks()
@@ -89,11 +75,13 @@ pub fn v1_to_v2(db: Arc<dyn Database>, config: Config) -> Result<()> {
let mut counter = 0;
// Get all candidate entries, approval entries and convert each of them.
for block in all_blocks {
for (_core_index, candidate_hash) in block.candidates() {
for (candidate_index, (_core_index, candidate_hash)) in
block.candidates().iter().enumerate()
{
// Loading the candidate will also perform the conversion to the updated format and
// return that represantation.
if let Some(candidate_entry) = backend
.load_candidate_entry_v1(&candidate_hash)
.load_candidate_entry_v1(&candidate_hash, candidate_index as CandidateIndex)
.map_err(|e| Error::InternalError(e))?
{
// Write the updated representation.
@@ -113,42 +101,8 @@ pub fn v1_to_v2(db: Arc<dyn Database>, config: Config) -> Result<()> {
Ok(())
}
// Checks if the migration doesn't leave the DB in an unsane state.
// This function is to be used in tests.
pub fn v1_to_v2_sanity_check(
db: Arc<dyn Database>,
config: Config,
expected_candidates: HashSet<CandidateHash>,
) -> Result<()> {
let backend = crate::DbBackend::new(db, config);
let all_blocks = backend
.load_all_blocks()
.unwrap()
.iter()
.map(|block_hash| backend.load_block_entry(block_hash).unwrap().unwrap())
.collect::<Vec<_>>();
let mut candidates = HashSet::new();
// Iterate all blocks and approval entries.
for block in all_blocks {
for (_core_index, candidate_hash) in block.candidates() {
// Loading the candidate will also perform the conversion to the updated format and
// return that represantation.
if let Some(candidate_entry) = backend.load_candidate_entry(&candidate_hash).unwrap() {
candidates.insert(candidate_entry.candidate.hash());
}
}
}
assert_eq!(candidates, expected_candidates);
Ok(())
}
// Fills the db with dummy data in v1 scheme.
pub fn v1_to_v2_fill_test_data<F>(
pub fn v1_fill_test_data<F>(
db: Arc<dyn Database>,
config: Config,
dummy_candidate_create: F,
@@ -21,145 +21,23 @@ use polkadot_node_primitives::approval::{v1::DelayTranche, v2::AssignmentCertV2}
use polkadot_node_subsystem::{SubsystemError, SubsystemResult};
use polkadot_node_subsystem_util::database::{DBTransaction, Database};
use polkadot_primitives::{
BlockNumber, CandidateHash, CandidateReceipt, CoreIndex, GroupIndex, Hash, SessionIndex,
ValidatorIndex, ValidatorSignature,
BlockNumber, CandidateHash, CandidateIndex, CandidateReceipt, CoreIndex, GroupIndex, Hash,
SessionIndex, ValidatorIndex, ValidatorSignature,
};
use sp_consensus_slots::Slot;
use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec};
use std::{collections::BTreeMap, sync::Arc};
use std::collections::BTreeMap;
use crate::{
backend::{Backend, BackendWriteOp, V1ReadBackend},
persisted_entries,
};
use crate::backend::V1ReadBackend;
const STORED_BLOCKS_KEY: &[u8] = b"Approvals_StoredBlocks";
use super::common::{block_entry_key, candidate_entry_key, load_decode, Config};
pub mod migration_helpers;
#[cfg(test)]
pub mod tests;
/// `DbBackend` is a concrete implementation of the higher-level Backend trait
pub struct DbBackend {
inner: Arc<dyn Database>,
config: Config,
}
impl DbBackend {
/// Create a new [`DbBackend`] with the supplied key-value store and
/// config.
pub fn new(db: Arc<dyn Database>, config: Config) -> Self {
DbBackend { inner: db, config }
}
}
impl V1ReadBackend for DbBackend {
fn load_candidate_entry_v1(
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<persisted_entries::CandidateEntry>> {
load_candidate_entry_v1(&*self.inner, &self.config, candidate_hash)
.map(|e| e.map(Into::into))
}
fn load_block_entry_v1(
&self,
block_hash: &Hash,
) -> SubsystemResult<Option<persisted_entries::BlockEntry>> {
load_block_entry_v1(&*self.inner, &self.config, block_hash).map(|e| e.map(Into::into))
}
}
impl Backend for DbBackend {
fn load_block_entry(
&self,
block_hash: &Hash,
) -> SubsystemResult<Option<persisted_entries::BlockEntry>> {
load_block_entry(&*self.inner, &self.config, block_hash).map(|e| e.map(Into::into))
}
fn load_candidate_entry(
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<persisted_entries::CandidateEntry>> {
load_candidate_entry(&*self.inner, &self.config, candidate_hash).map(|e| e.map(Into::into))
}
fn load_blocks_at_height(&self, block_height: &BlockNumber) -> SubsystemResult<Vec<Hash>> {
load_blocks_at_height(&*self.inner, &self.config, block_height)
}
fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>> {
load_all_blocks(&*self.inner, &self.config)
}
fn load_stored_blocks(&self) -> SubsystemResult<Option<StoredBlockRange>> {
load_stored_blocks(&*self.inner, &self.config)
}
/// Atomically write the list of operations, with later operations taking precedence over prior.
fn write<I>(&mut self, ops: I) -> SubsystemResult<()>
where
I: IntoIterator<Item = BackendWriteOp>,
{
let mut tx = DBTransaction::new();
for op in ops {
match op {
BackendWriteOp::WriteStoredBlockRange(stored_block_range) => {
tx.put_vec(
self.config.col_approval_data,
&STORED_BLOCKS_KEY,
stored_block_range.encode(),
);
},
BackendWriteOp::DeleteStoredBlockRange => {
tx.delete(self.config.col_approval_data, &STORED_BLOCKS_KEY);
},
BackendWriteOp::WriteBlocksAtHeight(h, blocks) => {
tx.put_vec(
self.config.col_approval_data,
&blocks_at_height_key(h),
blocks.encode(),
);
},
BackendWriteOp::DeleteBlocksAtHeight(h) => {
tx.delete(self.config.col_approval_data, &blocks_at_height_key(h));
},
BackendWriteOp::WriteBlockEntry(block_entry) => {
let block_entry: BlockEntry = block_entry.into();
tx.put_vec(
self.config.col_approval_data,
&block_entry_key(&block_entry.block_hash),
block_entry.encode(),
);
},
BackendWriteOp::DeleteBlockEntry(hash) => {
tx.delete(self.config.col_approval_data, &block_entry_key(&hash));
},
BackendWriteOp::WriteCandidateEntry(candidate_entry) => {
let candidate_entry: CandidateEntry = candidate_entry.into();
tx.put_vec(
self.config.col_approval_data,
&candidate_entry_key(&candidate_entry.candidate.hash()),
candidate_entry.encode(),
);
},
BackendWriteOp::DeleteCandidateEntry(candidate_hash) => {
tx.delete(self.config.col_approval_data, &candidate_entry_key(&candidate_hash));
},
}
}
self.inner.write(tx).map_err(|e| e.into())
}
}
/// A range from earliest..last block number stored within the DB.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct StoredBlockRange(pub BlockNumber, pub BlockNumber);
// slot_duration * 2 + DelayTranche gives the number of delay tranches since the
// unix epoch.
#[derive(Encode, Decode, Clone, Copy, Debug, PartialEq)]
@@ -168,13 +46,6 @@ pub struct Tick(u64);
/// Convenience type definition
pub type Bitfield = BitVec<u8, BitOrderLsb0>;
/// The database config.
#[derive(Debug, Clone, Copy)]
pub struct Config {
/// The column family in the database where data is stored.
pub col_approval_data: u32,
}
/// Details pertaining to our assignment on a block.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct OurAssignment {
@@ -259,118 +130,6 @@ impl From<Tick> for crate::Tick {
}
}
/// Errors while accessing things from the DB.
#[derive(Debug, derive_more::From, derive_more::Display)]
pub enum Error {
Io(std::io::Error),
InvalidDecoding(parity_scale_codec::Error),
InternalError(SubsystemError),
}
impl std::error::Error for Error {}
/// Result alias for DB errors.
pub type Result<T> = std::result::Result<T, Error>;
pub(crate) fn load_decode<D: Decode>(
store: &dyn Database,
col_approval_data: u32,
key: &[u8],
) -> Result<Option<D>> {
match store.get(col_approval_data, key)? {
None => Ok(None),
Some(raw) => D::decode(&mut &raw[..]).map(Some).map_err(Into::into),
}
}
/// The key a given block entry is stored under.
pub(crate) fn block_entry_key(block_hash: &Hash) -> [u8; 46] {
const BLOCK_ENTRY_PREFIX: [u8; 14] = *b"Approvals_blck";
let mut key = [0u8; 14 + 32];
key[0..14].copy_from_slice(&BLOCK_ENTRY_PREFIX);
key[14..][..32].copy_from_slice(block_hash.as_ref());
key
}
/// The key a given candidate entry is stored under.
pub(crate) fn candidate_entry_key(candidate_hash: &CandidateHash) -> [u8; 46] {
const CANDIDATE_ENTRY_PREFIX: [u8; 14] = *b"Approvals_cand";
let mut key = [0u8; 14 + 32];
key[0..14].copy_from_slice(&CANDIDATE_ENTRY_PREFIX);
key[14..][..32].copy_from_slice(candidate_hash.0.as_ref());
key
}
/// The key a set of block hashes corresponding to a block number is stored under.
pub(crate) fn blocks_at_height_key(block_number: BlockNumber) -> [u8; 16] {
const BLOCKS_AT_HEIGHT_PREFIX: [u8; 12] = *b"Approvals_at";
let mut key = [0u8; 12 + 4];
key[0..12].copy_from_slice(&BLOCKS_AT_HEIGHT_PREFIX);
block_number.using_encoded(|s| key[12..16].copy_from_slice(s));
key
}
/// Return all blocks which have entries in the DB, ascending, by height.
pub fn load_all_blocks(store: &dyn Database, config: &Config) -> SubsystemResult<Vec<Hash>> {
let mut hashes = Vec::new();
if let Some(stored_blocks) = load_stored_blocks(store, config)? {
for height in stored_blocks.0..stored_blocks.1 {
let blocks = load_blocks_at_height(store, config, &height)?;
hashes.extend(blocks);
}
}
Ok(hashes)
}
/// Load the stored-blocks key from the state.
pub fn load_stored_blocks(
store: &dyn Database,
config: &Config,
) -> SubsystemResult<Option<StoredBlockRange>> {
load_decode(store, config.col_approval_data, STORED_BLOCKS_KEY)
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a blocks-at-height entry for a given block number.
pub fn load_blocks_at_height(
store: &dyn Database,
config: &Config,
block_number: &BlockNumber,
) -> SubsystemResult<Vec<Hash>> {
load_decode(store, config.col_approval_data, &blocks_at_height_key(*block_number))
.map(|x| x.unwrap_or_default())
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a block entry from the aux store.
pub fn load_block_entry(
store: &dyn Database,
config: &Config,
block_hash: &Hash,
) -> SubsystemResult<Option<BlockEntry>> {
load_decode(store, config.col_approval_data, &block_entry_key(block_hash))
.map(|u: Option<BlockEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a candidate entry from the aux store in current version format.
pub fn load_candidate_entry(
store: &dyn Database,
config: &Config,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>> {
load_decode(store, config.col_approval_data, &candidate_entry_key(candidate_hash))
.map(|u: Option<CandidateEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a candidate entry from the aux store in v1 format.
pub fn load_candidate_entry_v1(
store: &dyn Database,
@@ -16,13 +16,22 @@
//! Tests for the aux-schema of approval voting.
use super::{DbBackend, StoredBlockRange, *};
use crate::{
approval_db::{
common::{migration_helpers::make_bitvec, DbBackend, StoredBlockRange, *},
v2::*,
v3::{load_block_entry_v2, load_candidate_entry_v2},
},
backend::{Backend, OverlayedBackend},
ops::{add_block_entry, canonicalize, force_approve, NewCandidateInfo},
};
use polkadot_primitives::{
BlockNumber, CandidateHash, CandidateReceipt, CoreIndex, GroupIndex, Hash,
};
use polkadot_node_subsystem_util::database::Database;
use polkadot_primitives::Id as ParaId;
use sp_consensus_slots::Slot;
use std::{collections::HashMap, sync::Arc};
use ::test_helpers::{dummy_candidate_receipt, dummy_candidate_receipt_bad_sig, dummy_hash};
@@ -60,10 +69,6 @@ fn make_block_entry(
}
}
fn make_bitvec(len: usize) -> BitVec<u8, BitOrderLsb0> {
bitvec::bitvec![u8, BitOrderLsb0; 0; len]
}
fn make_candidate(para_id: ParaId, relay_parent: Hash) -> CandidateReceipt {
let mut c = dummy_candidate_receipt(dummy_hash());
@@ -110,7 +115,10 @@ fn read_write() {
overlay_db.write_stored_block_range(range.clone());
overlay_db.write_blocks_at_height(1, at_height.clone());
overlay_db.write_block_entry(block_entry.clone().into());
overlay_db.write_candidate_entry(candidate_entry.clone().into());
overlay_db.write_candidate_entry(crate::persisted_entries::CandidateEntry::from_v2(
candidate_entry.clone(),
0,
));
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
@@ -118,11 +126,11 @@ fn read_write() {
assert_eq!(load_stored_blocks(store.as_ref(), &TEST_CONFIG).unwrap(), Some(range));
assert_eq!(load_blocks_at_height(store.as_ref(), &TEST_CONFIG, &1).unwrap(), at_height);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap(),
load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap(),
Some(block_entry.into())
);
assert_eq!(
load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash).unwrap(),
load_candidate_entry_v2(store.as_ref(), &TEST_CONFIG, &candidate_hash).unwrap(),
Some(candidate_entry.into()),
);
@@ -134,8 +142,8 @@ fn read_write() {
db.write(write_ops).unwrap();
assert!(load_blocks_at_height(store.as_ref(), &TEST_CONFIG, &1).unwrap().is_empty());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap().is_none());
assert!(load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash)
assert!(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap().is_none());
assert!(load_candidate_entry_v2(store.as_ref(), &TEST_CONFIG, &candidate_hash)
.unwrap()
.is_none());
}
@@ -196,25 +204,27 @@ fn add_block_entry_works() {
db.write(write_ops).unwrap();
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
Some(block_entry_a.into())
);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
Some(block_entry_b.into())
);
let candidate_entry_a = load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash_a)
.unwrap()
.unwrap();
let candidate_entry_a =
load_candidate_entry_v2(store.as_ref(), &TEST_CONFIG, &candidate_hash_a)
.unwrap()
.unwrap();
assert_eq!(
candidate_entry_a.block_assignments.keys().collect::<Vec<_>>(),
vec![&block_hash_a, &block_hash_b]
);
let candidate_entry_b = load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash_b)
.unwrap()
.unwrap();
let candidate_entry_b =
load_candidate_entry_v2(store.as_ref(), &TEST_CONFIG, &candidate_hash_b)
.unwrap()
.unwrap();
assert_eq!(candidate_entry_b.block_assignments.keys().collect::<Vec<_>>(), vec![&block_hash_b]);
}
@@ -243,11 +253,11 @@ fn add_block_entry_adds_child() {
block_entry_a.children.push(block_hash_b);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
Some(block_entry_a.into())
);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
Some(block_entry_b.into())
);
}
@@ -365,13 +375,15 @@ fn canonicalize_works() {
for (c_hash, in_blocks) in expected {
let (entry, in_blocks) = match in_blocks {
None => {
assert!(load_candidate_entry(store.as_ref(), &TEST_CONFIG, &c_hash)
assert!(load_candidate_entry_v2(store.as_ref(), &TEST_CONFIG, &c_hash)
.unwrap()
.is_none());
continue
},
Some(i) => (
load_candidate_entry(store.as_ref(), &TEST_CONFIG, &c_hash).unwrap().unwrap(),
load_candidate_entry_v2(store.as_ref(), &TEST_CONFIG, &c_hash)
.unwrap()
.unwrap(),
i,
),
};
@@ -388,13 +400,13 @@ fn canonicalize_works() {
for (hash, with_candidates) in expected {
let (entry, with_candidates) = match with_candidates {
None => {
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash)
assert!(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &hash)
.unwrap()
.is_none());
continue
},
Some(i) =>
(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash).unwrap().unwrap(), i),
(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &hash).unwrap().unwrap(), i),
};
assert_eq!(entry.candidates.len(), with_candidates.len());
@@ -510,22 +522,22 @@ fn force_approve_works() {
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a,)
assert!(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_a,)
.unwrap()
.unwrap()
.approved_bitfield
.all());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b,)
assert!(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_b,)
.unwrap()
.unwrap()
.approved_bitfield
.all());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_c,)
assert!(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_c,)
.unwrap()
.unwrap()
.approved_bitfield
.not_any());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_d,)
assert!(load_block_entry_v2(store.as_ref(), &TEST_CONFIG, &block_hash_d,)
.unwrap()
.unwrap()
.approved_bitfield
@@ -0,0 +1,237 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Approval DB migration helpers.
use super::*;
use crate::{
approval_db::common::{
block_entry_key, candidate_entry_key,
migration_helpers::{dummy_assignment_cert, make_bitvec},
Config, Error, Result, StoredBlockRange,
},
backend::{Backend, V2ReadBackend},
};
use polkadot_node_primitives::approval::v1::AssignmentCertKind;
use polkadot_node_subsystem_util::database::Database;
use sp_application_crypto::sp_core::H256;
use std::{collections::HashSet, sync::Arc};
/// Migrates `BlockEntry`, `CandidateEntry`, `ApprovalEntry` and `OurApproval` to version 3.
/// Returns on any error.
/// Must only be used in parachains DB migration code - `polkadot-service` crate.
pub fn v2_to_latest(db: Arc<dyn Database>, config: Config) -> Result<()> {
let mut backend = crate::DbBackend::new(db, config);
let all_blocks = backend
.load_all_blocks()
.map_err(|e| Error::InternalError(e))?
.iter()
.filter_map(|block_hash| {
backend
.load_block_entry_v2(block_hash)
.map_err(|e| Error::InternalError(e))
.ok()?
})
.collect::<Vec<_>>();
gum::info!(
target: crate::LOG_TARGET,
"Migrating candidate entries on top of {} blocks",
all_blocks.len()
);
let mut overlay = crate::OverlayedBackend::new(&backend);
let mut counter = 0;
// Get all candidate entries, approval entries and convert each of them.
for block in all_blocks {
for (candidate_index, (_core_index, candidate_hash)) in
block.candidates().iter().enumerate()
{
// Loading the candidate will also perform the conversion to the updated format and
// return that represantation.
if let Some(candidate_entry) = backend
.load_candidate_entry_v2(&candidate_hash, candidate_index as CandidateIndex)
.map_err(|e| Error::InternalError(e))?
{
// Write the updated representation.
overlay.write_candidate_entry(candidate_entry);
counter += 1;
}
}
overlay.write_block_entry(block);
}
gum::info!(target: crate::LOG_TARGET, "Migrated {} entries", counter);
// Commit all changes to DB.
let write_ops = overlay.into_write_ops();
backend.write(write_ops).unwrap();
Ok(())
}
// Checks if the migration doesn't leave the DB in an unsane state.
// This function is to be used in tests.
pub fn v1_to_latest_sanity_check(
db: Arc<dyn Database>,
config: Config,
expected_candidates: HashSet<CandidateHash>,
) -> Result<()> {
let backend = crate::DbBackend::new(db, config);
let all_blocks = backend
.load_all_blocks()
.unwrap()
.iter()
.map(|block_hash| backend.load_block_entry(block_hash).unwrap().unwrap())
.collect::<Vec<_>>();
let mut candidates = HashSet::new();
// Iterate all blocks and approval entries.
for block in all_blocks {
for (_core_index, candidate_hash) in block.candidates() {
// Loading the candidate will also perform the conversion to the updated format and
// return that represantation.
if let Some(candidate_entry) = backend.load_candidate_entry(&candidate_hash).unwrap() {
candidates.insert(candidate_entry.candidate.hash());
}
}
}
assert_eq!(candidates, expected_candidates);
Ok(())
}
// Fills the db with dummy data in v2 scheme.
pub fn v2_fill_test_data<F>(
db: Arc<dyn Database>,
config: Config,
dummy_candidate_create: F,
) -> Result<HashSet<CandidateHash>>
where
F: Fn(H256) -> CandidateReceipt<H256>,
{
let mut backend = crate::DbBackend::new(db.clone(), config);
let mut overlay_db = crate::OverlayedBackend::new(&backend);
let mut expected_candidates = HashSet::new();
const RELAY_BLOCK_COUNT: u32 = 10;
let range = StoredBlockRange(1, 11);
overlay_db.write_stored_block_range(range.clone());
for relay_number in 1..=RELAY_BLOCK_COUNT {
let relay_hash = Hash::repeat_byte(relay_number as u8);
let assignment_core_index = CoreIndex(relay_number);
let candidate = dummy_candidate_create(relay_hash);
let candidate_hash = candidate.hash();
let at_height = vec![relay_hash];
let block_entry = make_block_entry_v2(
relay_hash,
Default::default(),
relay_number,
vec![(assignment_core_index, candidate_hash)],
);
let dummy_assignment = crate::approval_db::v2::OurAssignment {
cert: dummy_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }).into(),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
};
let candidate_entry = crate::approval_db::v2::CandidateEntry {
candidate,
session: 123,
block_assignments: vec![(
relay_hash,
crate::approval_db::v2::ApprovalEntry {
tranches: Vec::new(),
backing_group: GroupIndex(1),
our_assignment: Some(dummy_assignment),
our_approval_sig: None,
approved: false,
assigned_validators: make_bitvec(1),
},
)]
.into_iter()
.collect(),
approvals: Default::default(),
};
overlay_db.write_blocks_at_height(relay_number, at_height.clone());
expected_candidates.insert(candidate_entry.candidate.hash());
db.write(write_candidate_entry_v2(candidate_entry, config)).unwrap();
db.write(write_block_entry_v2(block_entry, config)).unwrap();
}
let write_ops = overlay_db.into_write_ops();
backend.write(write_ops).unwrap();
Ok(expected_candidates)
}
fn make_block_entry_v2(
block_hash: Hash,
parent_hash: Hash,
block_number: BlockNumber,
candidates: Vec<(CoreIndex, CandidateHash)>,
) -> crate::approval_db::v2::BlockEntry {
crate::approval_db::v2::BlockEntry {
block_hash,
parent_hash,
block_number,
session: 1,
slot: Slot::from(1),
relay_vrf_story: [0u8; 32],
approved_bitfield: make_bitvec(candidates.len()),
distributed_assignments: make_bitvec(candidates.len()),
candidates,
children: Vec::new(),
}
}
// Low level DB helper to write a candidate entry in v1 scheme.
fn write_candidate_entry_v2(
candidate_entry: crate::approval_db::v2::CandidateEntry,
config: Config,
) -> DBTransaction {
let mut tx = DBTransaction::new();
tx.put_vec(
config.col_approval_data,
&candidate_entry_key(&candidate_entry.candidate.hash()),
candidate_entry.encode(),
);
tx
}
// Low level DB helper to write a block entry in v1 scheme.
fn write_block_entry_v2(
block_entry: crate::approval_db::v2::BlockEntry,
config: Config,
) -> DBTransaction {
let mut tx = DBTransaction::new();
tx.put_vec(
config.col_approval_data,
&block_entry_key(&block_entry.block_hash),
block_entry.encode(),
);
tx
}
@@ -0,0 +1,137 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Version 3 of the DB schema.
//!
//! Version 3 modifies the `our_approval` format of `ApprovalEntry`
//! and adds a new field `pending_signatures` for `BlockEntry`
use parity_scale_codec::{Decode, Encode};
use polkadot_node_primitives::approval::v2::CandidateBitfield;
use polkadot_node_subsystem::SubsystemResult;
use polkadot_node_subsystem_util::database::{DBTransaction, Database};
use polkadot_overseer::SubsystemError;
use polkadot_primitives::{
BlockNumber, CandidateHash, CandidateIndex, CandidateReceipt, CoreIndex, GroupIndex, Hash,
SessionIndex, ValidatorIndex, ValidatorSignature,
};
use sp_consensus_slots::Slot;
use std::collections::BTreeMap;
use super::common::{block_entry_key, candidate_entry_key, load_decode, Config};
/// Re-export this structs as v3 since they did not change between v2 and v3.
pub use super::v2::{Bitfield, OurAssignment, Tick, TrancheEntry};
pub mod migration_helpers;
#[cfg(test)]
pub mod tests;
/// Metadata about our approval signature
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct OurApproval {
/// The signature for the candidates hashes pointed by indices.
pub signature: ValidatorSignature,
/// The indices of the candidates signed in this approval.
pub signed_candidates_indices: CandidateBitfield,
}
/// Metadata regarding approval of a particular candidate within the context of some
/// particular block.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct ApprovalEntry {
pub tranches: Vec<TrancheEntry>,
pub backing_group: GroupIndex,
pub our_assignment: Option<OurAssignment>,
pub our_approval_sig: Option<OurApproval>,
// `n_validators` bits.
pub assigned_validators: Bitfield,
pub approved: bool,
}
/// Metadata regarding approval of a particular candidate.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct CandidateEntry {
pub candidate: CandidateReceipt,
pub session: SessionIndex,
// Assignments are based on blocks, so we need to track assignments separately
// based on the block we are looking at.
pub block_assignments: BTreeMap<Hash, ApprovalEntry>,
pub approvals: Bitfield,
}
/// Metadata regarding approval of a particular block, by way of approval of the
/// candidates contained within it.
#[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
/// author as a credential and used as input to approval assignment criteria.
pub relay_vrf_story: [u8; 32],
// The candidates included as-of this block and the index of the core they are
// leaving. Sorted ascending by core index.
pub candidates: Vec<(CoreIndex, CandidateHash)>,
// A bitfield where the i'th bit corresponds to the i'th candidate in `candidates`.
// The i'th bit is `true` iff the candidate has been approved in the context of this
// block. The block can be considered approved if the bitfield has all bits set to `true`.
pub approved_bitfield: Bitfield,
pub children: Vec<Hash>,
// A list of candidates we have checked, but didn't not sign and
// advertise the vote yet.
pub candidates_pending_signature: BTreeMap<CandidateIndex, CandidateSigningContext>,
// Assignments we already distributed. A 1 bit means the candidate index for which
// we already have sent out an assignment. We need this to avoid distributing
// multiple core assignments more than once.
pub distributed_assignments: Bitfield,
}
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
/// Context needed for creating an approval signature for a given candidate.
pub struct CandidateSigningContext {
/// The candidate hash, to be included in the signature.
pub candidate_hash: CandidateHash,
/// The latest tick we have to create and send the approval.
pub sign_no_later_than_tick: Tick,
}
/// Load a candidate entry from the aux store in v2 format.
pub fn load_candidate_entry_v2(
store: &dyn Database,
config: &Config,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<super::v2::CandidateEntry>> {
load_decode(store, config.col_approval_data, &candidate_entry_key(candidate_hash))
.map(|u: Option<super::v2::CandidateEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a block entry from the aux store in v2 format.
pub fn load_block_entry_v2(
store: &dyn Database,
config: &Config,
block_hash: &Hash,
) -> SubsystemResult<Option<super::v2::BlockEntry>> {
load_decode(store, config.col_approval_data, &block_entry_key(block_hash))
.map(|u: Option<super::v2::BlockEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
@@ -0,0 +1,575 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Tests for the aux-schema of approval voting.
use crate::{
approval_db::{
common::{migration_helpers::make_bitvec, DbBackend, StoredBlockRange, *},
v3::*,
},
backend::{Backend, OverlayedBackend},
ops::{add_block_entry, canonicalize, force_approve, NewCandidateInfo},
};
use polkadot_primitives::{
BlockNumber, CandidateHash, CandidateReceipt, CoreIndex, GroupIndex, Hash,
};
use polkadot_node_subsystem_util::database::Database;
use polkadot_primitives::Id as ParaId;
use sp_consensus_slots::Slot;
use std::{collections::HashMap, sync::Arc};
use ::test_helpers::{dummy_candidate_receipt, dummy_candidate_receipt_bad_sig, dummy_hash};
const DATA_COL: u32 = 0;
const NUM_COLUMNS: u32 = 1;
const TEST_CONFIG: Config = Config { col_approval_data: DATA_COL };
fn make_db() -> (DbBackend, Arc<dyn Database>) {
let db = kvdb_memorydb::create(NUM_COLUMNS);
let db = polkadot_node_subsystem_util::database::kvdb_impl::DbAdapter::new(db, &[]);
let db_writer: Arc<dyn Database> = Arc::new(db);
(DbBackend::new(db_writer.clone(), TEST_CONFIG), db_writer)
}
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],
approved_bitfield: make_bitvec(candidates.len()),
candidates,
children: Vec::new(),
candidates_pending_signature: Default::default(),
distributed_assignments: Default::default(),
}
}
fn make_candidate(para_id: ParaId, relay_parent: Hash) -> CandidateReceipt {
let mut c = dummy_candidate_receipt(dummy_hash());
c.descriptor.para_id = para_id;
c.descriptor.relay_parent = relay_parent;
c
}
#[test]
fn read_write() {
let (mut db, store) = make_db();
let hash_a = Hash::repeat_byte(1);
let hash_b = Hash::repeat_byte(2);
let candidate_hash = dummy_candidate_receipt_bad_sig(dummy_hash(), None).hash();
let range = StoredBlockRange(10, 20);
let at_height = vec![hash_a, hash_b];
let block_entry =
make_block_entry(hash_a, Default::default(), 1, vec![(CoreIndex(0), candidate_hash)]);
let candidate_entry = CandidateEntry {
candidate: dummy_candidate_receipt_bad_sig(dummy_hash(), None),
session: 5,
block_assignments: vec![(
hash_a,
ApprovalEntry {
tranches: Vec::new(),
backing_group: GroupIndex(1),
our_assignment: None,
our_approval_sig: None,
assigned_validators: Default::default(),
approved: false,
},
)]
.into_iter()
.collect(),
approvals: Default::default(),
};
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(range.clone());
overlay_db.write_blocks_at_height(1, at_height.clone());
overlay_db.write_block_entry(block_entry.clone().into());
overlay_db.write_candidate_entry(candidate_entry.clone().into());
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(load_stored_blocks(store.as_ref(), &TEST_CONFIG).unwrap(), Some(range));
assert_eq!(load_blocks_at_height(store.as_ref(), &TEST_CONFIG, &1).unwrap(), at_height);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap(),
Some(block_entry.into())
);
assert_eq!(
load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash).unwrap(),
Some(candidate_entry.into()),
);
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.delete_blocks_at_height(1);
overlay_db.delete_block_entry(&hash_a);
overlay_db.delete_candidate_entry(&candidate_hash);
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert!(load_blocks_at_height(store.as_ref(), &TEST_CONFIG, &1).unwrap().is_empty());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap().is_none());
assert!(load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash)
.unwrap()
.is_none());
}
#[test]
fn add_block_entry_works() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let candidate_receipt_a = make_candidate(ParaId::from(1_u32), parent_hash);
let candidate_receipt_b = make_candidate(ParaId::from(2_u32), parent_hash);
let candidate_hash_a = candidate_receipt_a.hash();
let candidate_hash_b = candidate_receipt_b.hash();
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 mut new_candidate_info = HashMap::new();
new_candidate_info
.insert(candidate_hash_a, NewCandidateInfo::new(candidate_receipt_a, GroupIndex(0), None));
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |h| {
new_candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
new_candidate_info
.insert(candidate_hash_b, NewCandidateInfo::new(candidate_receipt_b, GroupIndex(1), None));
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |h| {
new_candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
Some(block_entry_a.into())
);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
Some(block_entry_b.into())
);
let candidate_entry_a = load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash_a)
.unwrap()
.unwrap();
assert_eq!(
candidate_entry_a.block_assignments.keys().collect::<Vec<_>>(),
vec![&block_hash_a, &block_hash_b]
);
let candidate_entry_b = load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash_b)
.unwrap()
.unwrap();
assert_eq!(candidate_entry_b.block_assignments.keys().collect::<Vec<_>>(), vec![&block_hash_b]);
}
#[test]
fn add_block_entry_adds_child() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
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());
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |_| None).unwrap();
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |_| None).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
block_entry_a.children.push(block_hash_b);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
Some(block_entry_a.into())
);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
Some(block_entry_b.into())
);
}
#[test]
fn canonicalize_works() {
let (mut db, store) = make_db();
// -> B1 -> C1 -> D1
// A -> B2 -> C2 -> D2
//
// We'll canonicalize C1. Everytning except D1 should disappear.
//
// Candidates:
// Cand1 in B2
// Cand2 in C2
// Cand3 in C2 and D1
// Cand4 in D1
// Cand5 in D2
// Only Cand3 and Cand4 should remain after canonicalize.
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(StoredBlockRange(1, 5));
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let genesis = Hash::repeat_byte(0);
let block_hash_a = Hash::repeat_byte(1);
let block_hash_b1 = Hash::repeat_byte(2);
let block_hash_b2 = Hash::repeat_byte(3);
let block_hash_c1 = Hash::repeat_byte(4);
let block_hash_c2 = Hash::repeat_byte(5);
let block_hash_d1 = Hash::repeat_byte(6);
let block_hash_d2 = Hash::repeat_byte(7);
let candidate_receipt_genesis = make_candidate(ParaId::from(1_u32), genesis);
let candidate_receipt_a = make_candidate(ParaId::from(2_u32), block_hash_a);
let candidate_receipt_b = make_candidate(ParaId::from(3_u32), block_hash_a);
let candidate_receipt_b1 = make_candidate(ParaId::from(4_u32), block_hash_b1);
let candidate_receipt_c1 = make_candidate(ParaId::from(5_u32), block_hash_c1);
let cand_hash_1 = candidate_receipt_genesis.hash();
let cand_hash_2 = candidate_receipt_a.hash();
let cand_hash_3 = candidate_receipt_b.hash();
let cand_hash_4 = candidate_receipt_b1.hash();
let cand_hash_5 = candidate_receipt_c1.hash();
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, block_hash_c2, 4, vec![(CoreIndex(0), cand_hash_5)]);
let candidate_info = {
let mut candidate_info = HashMap::new();
candidate_info.insert(
cand_hash_1,
NewCandidateInfo::new(candidate_receipt_genesis, GroupIndex(1), None),
);
candidate_info
.insert(cand_hash_2, NewCandidateInfo::new(candidate_receipt_a, GroupIndex(2), None));
candidate_info
.insert(cand_hash_3, NewCandidateInfo::new(candidate_receipt_b, GroupIndex(3), None));
candidate_info
.insert(cand_hash_4, NewCandidateInfo::new(candidate_receipt_b1, GroupIndex(4), None));
candidate_info
.insert(cand_hash_5, NewCandidateInfo::new(candidate_receipt_c1, GroupIndex(5), None));
candidate_info
};
// now insert all the blocks.
let blocks = vec![
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(),
];
let mut overlay_db = OverlayedBackend::new(&db);
for block_entry in blocks {
add_block_entry(&mut overlay_db, block_entry.into(), n_validators, |h| {
candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
}
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let check_candidates_in_store = |expected: Vec<(CandidateHash, Option<Vec<_>>)>| {
for (c_hash, in_blocks) in expected {
let (entry, in_blocks) = match in_blocks {
None => {
assert!(load_candidate_entry(store.as_ref(), &TEST_CONFIG, &c_hash)
.unwrap()
.is_none());
continue
},
Some(i) => (
load_candidate_entry(store.as_ref(), &TEST_CONFIG, &c_hash).unwrap().unwrap(),
i,
),
};
assert_eq!(entry.block_assignments.len(), in_blocks.len());
for x in in_blocks {
assert!(entry.block_assignments.contains_key(&x));
}
}
};
let check_blocks_in_store = |expected: Vec<(Hash, Option<Vec<_>>)>| {
for (hash, with_candidates) in expected {
let (entry, with_candidates) = match with_candidates {
None => {
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash)
.unwrap()
.is_none());
continue
},
Some(i) =>
(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash).unwrap().unwrap(), i),
};
assert_eq!(entry.candidates.len(), with_candidates.len());
for x in with_candidates {
assert!(entry.candidates.iter().any(|(_, c)| c == &x));
}
}
};
check_candidates_in_store(vec![
(cand_hash_1, Some(vec![block_hash_b2])),
(cand_hash_2, Some(vec![block_hash_c2])),
(cand_hash_3, Some(vec![block_hash_c2, block_hash_d1])),
(cand_hash_4, Some(vec![block_hash_d1])),
(cand_hash_5, Some(vec![block_hash_d2])),
]);
check_blocks_in_store(vec![
(block_hash_a, Some(vec![])),
(block_hash_b1, Some(vec![])),
(block_hash_b2, Some(vec![cand_hash_1])),
(block_hash_c1, Some(vec![])),
(block_hash_c2, Some(vec![cand_hash_2, cand_hash_3])),
(block_hash_d1, Some(vec![cand_hash_3, cand_hash_4])),
(block_hash_d2, Some(vec![cand_hash_5])),
]);
let mut overlay_db = OverlayedBackend::new(&db);
canonicalize(&mut overlay_db, 3, block_hash_c1).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_stored_blocks(store.as_ref(), &TEST_CONFIG).unwrap().unwrap(),
StoredBlockRange(4, 5)
);
check_candidates_in_store(vec![
(cand_hash_1, None),
(cand_hash_2, None),
(cand_hash_3, Some(vec![block_hash_d1])),
(cand_hash_4, Some(vec![block_hash_d1])),
(cand_hash_5, None),
]);
check_blocks_in_store(vec![
(block_hash_a, None),
(block_hash_b1, None),
(block_hash_b2, None),
(block_hash_c1, None),
(block_hash_c2, None),
(block_hash_d1, Some(vec![cand_hash_3, cand_hash_4])),
(block_hash_d2, None),
]);
}
#[test]
fn force_approve_works() {
let (mut db, store) = make_db();
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(StoredBlockRange(1, 4));
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).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::new(
make_candidate(ParaId::from(1_u32), Default::default()),
GroupIndex(1),
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(),
];
let mut overlay_db = OverlayedBackend::new(&db);
for block_entry in blocks {
add_block_entry(&mut overlay_db, block_entry.into(), n_validators, |h| {
candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
}
let approved_hashes = force_approve(&mut overlay_db, block_hash_d, 2).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a,)
.unwrap()
.unwrap()
.approved_bitfield
.all());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b,)
.unwrap()
.unwrap()
.approved_bitfield
.all());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_c,)
.unwrap()
.unwrap()
.approved_bitfield
.not_any());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_d,)
.unwrap()
.unwrap()
.approved_bitfield
.not_any());
assert_eq!(approved_hashes, vec![block_hash_b, block_hash_a]);
}
#[test]
fn load_all_blocks_works() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let block_hash_c = Hash::repeat_byte(42);
let block_number = 10;
let block_entry_a = make_block_entry(block_hash_a, parent_hash, block_number, vec![]);
let block_entry_b = make_block_entry(block_hash_b, parent_hash, block_number, vec![]);
let block_entry_c = make_block_entry(block_hash_c, block_hash_a, block_number + 1, vec![]);
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |_| None).unwrap();
// add C before B to test sorting.
add_block_entry(&mut overlay_db, block_entry_c.clone().into(), n_validators, |_| None).unwrap();
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |_| None).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_all_blocks(store.as_ref(), &TEST_CONFIG).unwrap(),
vec![block_hash_a, block_hash_b, block_hash_c],
)
}
@@ -22,12 +22,12 @@
//! before any commit to the underlying storage is made.
use polkadot_node_subsystem::SubsystemResult;
use polkadot_primitives::{BlockNumber, CandidateHash, Hash};
use polkadot_primitives::{BlockNumber, CandidateHash, CandidateIndex, Hash};
use std::collections::HashMap;
use super::{
approval_db::v2::StoredBlockRange,
approval_db::common::StoredBlockRange,
persisted_entries::{BlockEntry, CandidateEntry},
};
@@ -72,12 +72,26 @@ pub trait V1ReadBackend: Backend {
fn load_candidate_entry_v1(
&self,
candidate_hash: &CandidateHash,
candidate_index: CandidateIndex,
) -> SubsystemResult<Option<CandidateEntry>>;
/// Load a block entry from the DB with scheme version 1.
fn load_block_entry_v1(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>>;
}
/// A read only backend to enable db migration from version 2 of DB.
pub trait V2ReadBackend: Backend {
/// Load a candidate entry from the DB with scheme version 1.
fn load_candidate_entry_v2(
&self,
candidate_hash: &CandidateHash,
candidate_index: CandidateIndex,
) -> SubsystemResult<Option<CandidateEntry>>;
/// Load a block entry from the DB with scheme version 1.
fn load_block_entry_v2(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>>;
}
// Status of block range in the `OverlayedBackend`.
#[derive(PartialEq)]
enum BlockRangeStatus {
@@ -56,7 +56,7 @@ use futures::{channel::oneshot, prelude::*};
use std::collections::HashMap;
use super::approval_db::v2;
use super::approval_db::v3;
use crate::{
backend::{Backend, OverlayedBackend},
criteria::{AssignmentCriteria, OurAssignment},
@@ -512,7 +512,7 @@ pub(crate) async fn handle_new_head<Context, B: Backend>(
ctx.send_message(ChainSelectionMessage::Approved(block_hash)).await;
}
let block_entry = v2::BlockEntry {
let block_entry = v3::BlockEntry {
block_hash,
parent_hash: block_header.parent_hash,
block_number: block_header.number,
@@ -525,6 +525,7 @@ pub(crate) async fn handle_new_head<Context, B: Backend>(
.collect(),
approved_bitfield,
children: Vec::new(),
candidates_pending_signature: Default::default(),
distributed_assignments: Default::default(),
};
@@ -604,7 +605,10 @@ pub(crate) async fn handle_new_head<Context, B: Backend>(
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{approval_db::v2::DbBackend, RuntimeInfo, RuntimeInfoConfig};
use crate::{
approval_db::common::{load_block_entry, DbBackend},
RuntimeInfo, RuntimeInfoConfig,
};
use ::test_helpers::{dummy_candidate_receipt, dummy_hash};
use assert_matches::assert_matches;
use polkadot_node_primitives::{
@@ -627,7 +631,7 @@ pub(crate) mod tests {
pub(crate) use sp_runtime::{Digest, DigestItem};
use std::{pin::Pin, sync::Arc};
use crate::{approval_db::v2::Config as DatabaseConfig, criteria, BlockEntry};
use crate::{approval_db::common::Config as DatabaseConfig, criteria, BlockEntry};
const DATA_COL: u32 = 0;
@@ -1347,7 +1351,7 @@ pub(crate) mod tests {
let (state, mut session_info_provider) = single_session_state();
overlay_db.write_block_entry(
v2::BlockEntry {
v3::BlockEntry {
block_hash: parent_hash,
parent_hash: Default::default(),
block_number: 4,
@@ -1357,6 +1361,7 @@ pub(crate) mod tests {
candidates: Vec::new(),
approved_bitfield: Default::default(),
children: Vec::new(),
candidates_pending_signature: Default::default(),
distributed_assignments: Default::default(),
}
.into(),
@@ -1389,11 +1394,10 @@ pub(crate) mod tests {
assert_eq!(candidates[1].1.approvals().len(), 6);
// the first candidate should be insta-approved
// the second should not
let entry: BlockEntry =
v2::load_block_entry(db_writer.as_ref(), &TEST_CONFIG, &hash)
.unwrap()
.unwrap()
.into();
let entry: BlockEntry = load_block_entry(db_writer.as_ref(), &TEST_CONFIG, &hash)
.unwrap()
.unwrap()
.into();
assert!(entry.is_candidate_approved(&candidates[0].0));
assert!(!entry.is_candidate_approved(&candidates[1].0));
})
File diff suppressed because it is too large Load Diff
@@ -25,7 +25,7 @@ use polkadot_primitives::{BlockNumber, CandidateHash, CandidateReceipt, GroupInd
use std::collections::{hash_map::Entry, BTreeMap, HashMap};
use super::{
approval_db::v2::{OurAssignment, StoredBlockRange},
approval_db::{common::StoredBlockRange, v2::OurAssignment},
backend::{Backend, OverlayedBackend},
persisted_entries::{ApprovalEntry, BlockEntry, CandidateEntry},
LOG_TARGET,
@@ -20,13 +20,14 @@
//! Within that context, things are plain-old-data. Within this module,
//! data and logic are intertwined.
use itertools::Itertools;
use polkadot_node_primitives::approval::{
v1::{DelayTranche, RelayVRFStory},
v2::{AssignmentCertV2, CandidateBitfield},
};
use polkadot_primitives::{
BlockNumber, CandidateHash, CandidateReceipt, CoreIndex, GroupIndex, Hash, SessionIndex,
ValidatorIndex, ValidatorSignature,
BlockNumber, CandidateHash, CandidateIndex, CandidateReceipt, CoreIndex, GroupIndex, Hash,
SessionIndex, ValidatorIndex, ValidatorSignature,
};
use sp_consensus_slots::Slot;
@@ -76,6 +77,45 @@ impl From<TrancheEntry> for crate::approval_db::v2::TrancheEntry {
}
}
impl From<crate::approval_db::v3::OurApproval> for OurApproval {
fn from(approval: crate::approval_db::v3::OurApproval) -> Self {
Self {
signature: approval.signature,
signed_candidates_indices: approval.signed_candidates_indices,
}
}
}
impl From<OurApproval> for crate::approval_db::v3::OurApproval {
fn from(approval: OurApproval) -> Self {
Self {
signature: approval.signature,
signed_candidates_indices: approval.signed_candidates_indices,
}
}
}
/// Metadata about our approval signature
#[derive(Debug, Clone, PartialEq)]
pub struct OurApproval {
/// The signature for the candidates hashes pointed by indices.
pub signature: ValidatorSignature,
/// The indices of the candidates signed in this approval.
pub signed_candidates_indices: CandidateBitfield,
}
impl OurApproval {
/// Converts a ValidatorSignature to an OurApproval.
/// It used in converting the database from v1 to latest.
pub fn from_v1(value: ValidatorSignature, candidate_index: CandidateIndex) -> Self {
Self { signature: value, signed_candidates_indices: candidate_index.into() }
}
/// Converts a ValidatorSignature to an OurApproval.
/// It used in converting the database from v2 to latest.
pub fn from_v2(value: ValidatorSignature, candidate_index: CandidateIndex) -> Self {
Self::from_v1(value, candidate_index)
}
}
/// Metadata regarding approval of a particular candidate within the context of some
/// particular block.
#[derive(Debug, Clone, PartialEq)]
@@ -83,7 +123,7 @@ pub struct ApprovalEntry {
tranches: Vec<TrancheEntry>,
backing_group: GroupIndex,
our_assignment: Option<OurAssignment>,
our_approval_sig: Option<ValidatorSignature>,
our_approval_sig: Option<OurApproval>,
// `n_validators` bits.
assigned_validators: Bitfield,
approved: bool,
@@ -95,7 +135,7 @@ impl ApprovalEntry {
tranches: Vec<TrancheEntry>,
backing_group: GroupIndex,
our_assignment: Option<OurAssignment>,
our_approval_sig: Option<ValidatorSignature>,
our_approval_sig: Option<OurApproval>,
// `n_validators` bits.
assigned_validators: Bitfield,
approved: bool,
@@ -137,7 +177,7 @@ impl ApprovalEntry {
}
/// Import our local approval vote signature for this candidate.
pub fn import_approval_sig(&mut self, approval_sig: ValidatorSignature) {
pub fn import_approval_sig(&mut self, approval_sig: OurApproval) {
self.our_approval_sig = Some(approval_sig);
}
@@ -224,7 +264,7 @@ impl ApprovalEntry {
/// Get the assignment cert & approval signature.
///
/// The approval signature will only be `Some` if the assignment is too.
pub fn local_statements(&self) -> (Option<OurAssignment>, Option<ValidatorSignature>) {
pub fn local_statements(&self) -> (Option<OurAssignment>, Option<OurApproval>) {
let approval_sig = self.our_approval_sig.clone();
if let Some(our_assignment) = self.our_assignment.as_ref().filter(|a| a.triggered()) {
(Some(our_assignment.clone()), approval_sig)
@@ -232,10 +272,44 @@ impl ApprovalEntry {
(None, None)
}
}
// Convert an ApprovalEntry from v1 version to latest version
pub fn from_v1(
value: crate::approval_db::v1::ApprovalEntry,
candidate_index: CandidateIndex,
) -> Self {
ApprovalEntry {
tranches: value.tranches.into_iter().map(|tranche| tranche.into()).collect(),
backing_group: value.backing_group,
our_assignment: value.our_assignment.map(|assignment| assignment.into()),
our_approval_sig: value
.our_approval_sig
.map(|sig| OurApproval::from_v1(sig, candidate_index)),
assigned_validators: value.assignments,
approved: value.approved,
}
}
// Convert an ApprovalEntry from v1 version to latest version
pub fn from_v2(
value: crate::approval_db::v2::ApprovalEntry,
candidate_index: CandidateIndex,
) -> Self {
ApprovalEntry {
tranches: value.tranches.into_iter().map(|tranche| tranche.into()).collect(),
backing_group: value.backing_group,
our_assignment: value.our_assignment.map(|assignment| assignment.into()),
our_approval_sig: value
.our_approval_sig
.map(|sig| OurApproval::from_v2(sig, candidate_index)),
assigned_validators: value.assigned_validators,
approved: value.approved,
}
}
}
impl From<crate::approval_db::v2::ApprovalEntry> for ApprovalEntry {
fn from(entry: crate::approval_db::v2::ApprovalEntry) -> Self {
impl From<crate::approval_db::v3::ApprovalEntry> for ApprovalEntry {
fn from(entry: crate::approval_db::v3::ApprovalEntry) -> Self {
ApprovalEntry {
tranches: entry.tranches.into_iter().map(Into::into).collect(),
backing_group: entry.backing_group,
@@ -247,7 +321,7 @@ impl From<crate::approval_db::v2::ApprovalEntry> for ApprovalEntry {
}
}
impl From<ApprovalEntry> for crate::approval_db::v2::ApprovalEntry {
impl From<ApprovalEntry> for crate::approval_db::v3::ApprovalEntry {
fn from(entry: ApprovalEntry) -> Self {
Self {
tranches: entry.tranches.into_iter().map(Into::into).collect(),
@@ -303,10 +377,44 @@ impl CandidateEntry {
pub fn approval_entry(&self, block_hash: &Hash) -> Option<&ApprovalEntry> {
self.block_assignments.get(block_hash)
}
/// Convert a CandidateEntry from a v1 to its latest equivalent.
pub fn from_v1(
value: crate::approval_db::v1::CandidateEntry,
candidate_index: CandidateIndex,
) -> Self {
Self {
approvals: value.approvals,
block_assignments: value
.block_assignments
.into_iter()
.map(|(h, ae)| (h, ApprovalEntry::from_v1(ae, candidate_index)))
.collect(),
candidate: value.candidate,
session: value.session,
}
}
/// Convert a CandidateEntry from a v2 to its latest equivalent.
pub fn from_v2(
value: crate::approval_db::v2::CandidateEntry,
candidate_index: CandidateIndex,
) -> Self {
Self {
approvals: value.approvals,
block_assignments: value
.block_assignments
.into_iter()
.map(|(h, ae)| (h, ApprovalEntry::from_v2(ae, candidate_index)))
.collect(),
candidate: value.candidate,
session: value.session,
}
}
}
impl From<crate::approval_db::v2::CandidateEntry> for CandidateEntry {
fn from(entry: crate::approval_db::v2::CandidateEntry) -> Self {
impl From<crate::approval_db::v3::CandidateEntry> for CandidateEntry {
fn from(entry: crate::approval_db::v3::CandidateEntry) -> Self {
CandidateEntry {
candidate: entry.candidate,
session: entry.session,
@@ -320,7 +428,7 @@ impl From<crate::approval_db::v2::CandidateEntry> for CandidateEntry {
}
}
impl From<CandidateEntry> for crate::approval_db::v2::CandidateEntry {
impl From<CandidateEntry> for crate::approval_db::v3::CandidateEntry {
fn from(entry: CandidateEntry) -> Self {
Self {
candidate: entry.candidate,
@@ -353,12 +461,21 @@ pub struct BlockEntry {
// block. The block can be considered approved if the bitfield has all bits set to `true`.
pub approved_bitfield: Bitfield,
pub children: Vec<Hash>,
// A list of candidates we have checked, but didn't not sign and
// advertise the vote yet.
candidates_pending_signature: BTreeMap<CandidateIndex, CandidateSigningContext>,
// A list of assignments for which we already distributed the assignment.
// We use this to ensure we don't distribute multiple core assignments twice as we track
// individual wakeups for each core.
distributed_assignments: Bitfield,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CandidateSigningContext {
pub candidate_hash: CandidateHash,
pub sign_no_later_than_tick: Tick,
}
impl BlockEntry {
/// Mark a candidate as fully approved in the bitfield.
pub fn mark_approved_by_hash(&mut self, candidate_hash: &CandidateHash) {
@@ -447,10 +564,97 @@ impl BlockEntry {
distributed
}
/// Defer signing and issuing an approval for a candidate no later than the specified tick
pub fn defer_candidate_signature(
&mut self,
candidate_index: CandidateIndex,
candidate_hash: CandidateHash,
sign_no_later_than_tick: Tick,
) -> Option<CandidateSigningContext> {
self.candidates_pending_signature.insert(
candidate_index,
CandidateSigningContext { candidate_hash, sign_no_later_than_tick },
)
}
/// Returns the number of candidates waiting for an approval to be issued.
pub fn num_candidates_pending_signature(&self) -> usize {
self.candidates_pending_signature.len()
}
/// Return if we have candidates waiting for signature to be issued
pub fn has_candidates_pending_signature(&self) -> bool {
!self.candidates_pending_signature.is_empty()
}
/// Candidate hashes for candidates pending signatures
fn candidate_hashes_pending_signature(&self) -> Vec<CandidateHash> {
self.candidates_pending_signature
.values()
.map(|unsigned_approval| unsigned_approval.candidate_hash)
.collect()
}
/// Candidate indices for candidates pending signature
fn candidate_indices_pending_signature(&self) -> Option<CandidateBitfield> {
self.candidates_pending_signature
.keys()
.map(|val| *val)
.collect_vec()
.try_into()
.ok()
}
/// Returns a list of candidates hashes that need need signature created at the current tick:
/// This might happen in other of the two reasons:
/// 1. We queued more than max_approval_coalesce_count candidates.
/// 2. We have candidates that waiting in the queue past their `sign_no_later_than_tick`
///
/// Additionally, we also return the first tick when we will have to create a signature,
/// so that the caller can arm the timer if it is not already armed.
pub fn get_candidates_that_need_signature(
&self,
tick_now: Tick,
max_approval_coalesce_count: u32,
) -> (Option<(Vec<CandidateHash>, CandidateBitfield)>, Option<Tick>) {
let sign_no_later_than_tick = self
.candidates_pending_signature
.values()
.min_by(|a, b| a.sign_no_later_than_tick.cmp(&b.sign_no_later_than_tick))
.map(|val| val.sign_no_later_than_tick);
if let Some(sign_no_later_than_tick) = sign_no_later_than_tick {
if sign_no_later_than_tick <= tick_now ||
self.num_candidates_pending_signature() >= max_approval_coalesce_count as usize
{
(
self.candidate_indices_pending_signature().and_then(|candidate_indices| {
Some((self.candidate_hashes_pending_signature(), candidate_indices))
}),
Some(sign_no_later_than_tick),
)
} else {
// We can still wait for other candidates to queue in, so just make sure
// we wake up at the tick we have to sign the longest waiting candidate.
(Default::default(), Some(sign_no_later_than_tick))
}
} else {
// No cached candidates, nothing to do here, this just means the timer fired,
// but the signatures were already sent because we gathered more than
// max_approval_coalesce_count.
(Default::default(), sign_no_later_than_tick)
}
}
/// Clears the candidates pending signature because the approval was issued.
pub fn issued_approval(&mut self) {
self.candidates_pending_signature.clear();
}
}
impl From<crate::approval_db::v2::BlockEntry> for BlockEntry {
fn from(entry: crate::approval_db::v2::BlockEntry) -> Self {
impl From<crate::approval_db::v3::BlockEntry> for BlockEntry {
fn from(entry: crate::approval_db::v3::BlockEntry) -> Self {
BlockEntry {
block_hash: entry.block_hash,
parent_hash: entry.parent_hash,
@@ -461,6 +665,11 @@ impl From<crate::approval_db::v2::BlockEntry> for BlockEntry {
candidates: entry.candidates,
approved_bitfield: entry.approved_bitfield,
children: entry.children,
candidates_pending_signature: entry
.candidates_pending_signature
.into_iter()
.map(|(candidate_index, signing_context)| (candidate_index, signing_context.into()))
.collect(),
distributed_assignments: entry.distributed_assignments,
}
}
@@ -479,11 +688,30 @@ impl From<crate::approval_db::v1::BlockEntry> for BlockEntry {
approved_bitfield: entry.approved_bitfield,
children: entry.children,
distributed_assignments: Default::default(),
candidates_pending_signature: Default::default(),
}
}
}
impl From<BlockEntry> for crate::approval_db::v2::BlockEntry {
impl From<crate::approval_db::v2::BlockEntry> for BlockEntry {
fn from(entry: crate::approval_db::v2::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),
candidates: entry.candidates,
approved_bitfield: entry.approved_bitfield,
children: entry.children,
distributed_assignments: entry.distributed_assignments,
candidates_pending_signature: Default::default(),
}
}
}
impl From<BlockEntry> for crate::approval_db::v3::BlockEntry {
fn from(entry: BlockEntry) -> Self {
Self {
block_hash: entry.block_hash,
@@ -495,36 +723,30 @@ impl From<BlockEntry> for crate::approval_db::v2::BlockEntry {
candidates: entry.candidates,
approved_bitfield: entry.approved_bitfield,
children: entry.children,
candidates_pending_signature: entry
.candidates_pending_signature
.into_iter()
.map(|(candidate_index, signing_context)| (candidate_index, signing_context.into()))
.collect(),
distributed_assignments: entry.distributed_assignments,
}
}
}
/// Migration helpers.
impl From<crate::approval_db::v1::CandidateEntry> for CandidateEntry {
fn from(value: crate::approval_db::v1::CandidateEntry) -> Self {
impl From<crate::approval_db::v3::CandidateSigningContext> for CandidateSigningContext {
fn from(signing_context: crate::approval_db::v3::CandidateSigningContext) -> Self {
Self {
approvals: value.approvals,
block_assignments: value
.block_assignments
.into_iter()
.map(|(h, ae)| (h, ae.into()))
.collect(),
candidate: value.candidate,
session: value.session,
candidate_hash: signing_context.candidate_hash,
sign_no_later_than_tick: signing_context.sign_no_later_than_tick.into(),
}
}
}
impl From<crate::approval_db::v1::ApprovalEntry> for ApprovalEntry {
fn from(value: crate::approval_db::v1::ApprovalEntry) -> Self {
ApprovalEntry {
tranches: value.tranches.into_iter().map(|tranche| tranche.into()).collect(),
backing_group: value.backing_group,
our_assignment: value.our_assignment.map(|assignment| assignment.into()),
our_approval_sig: value.our_approval_sig,
assigned_validators: value.assignments,
approved: value.approved,
impl From<CandidateSigningContext> for crate::approval_db::v3::CandidateSigningContext {
fn from(signing_context: CandidateSigningContext) -> Self {
Self {
candidate_hash: signing_context.candidate_hash,
sign_no_later_than_tick: signing_context.sign_no_later_than_tick.into(),
}
}
}
+576 -5
View File
@@ -37,8 +37,8 @@ use polkadot_node_subsystem_test_helpers as test_helpers;
use polkadot_node_subsystem_util::TimeoutExt;
use polkadot_overseer::HeadSupportsParachains;
use polkadot_primitives::{
vstaging::NodeFeatures, CandidateCommitments, CandidateEvent, CoreIndex, GroupIndex, Header,
Id as ParaId, IndexedVec, ValidationCode, ValidatorSignature,
vstaging::NodeFeatures, ApprovalVote, CandidateCommitments, CandidateEvent, CoreIndex,
GroupIndex, Header, Id as ParaId, IndexedVec, ValidationCode, ValidatorSignature,
};
use std::time::Duration;
@@ -56,7 +56,7 @@ use std::{
};
use super::{
approval_db::v2::StoredBlockRange,
approval_db::common::StoredBlockRange,
backend::BackendWriteOp,
import::tests::{
garbage_vrf_signature, AllowedSlots, BabeEpoch, BabeEpochConfiguration,
@@ -116,7 +116,7 @@ fn make_sync_oracle(val: bool) -> (Box<dyn SyncOracle + Send>, TestSyncOracleHan
#[cfg(test)]
pub mod test_constants {
use crate::approval_db::v2::Config as DatabaseConfig;
use crate::approval_db::common::Config as DatabaseConfig;
const DATA_COL: u32 = 0;
pub(crate) const NUM_COLUMNS: u32 = 1;
@@ -281,6 +281,7 @@ impl V1ReadBackend for TestStoreInner {
fn load_candidate_entry_v1(
&self,
candidate_hash: &CandidateHash,
_candidate_index: CandidateIndex,
) -> SubsystemResult<Option<CandidateEntry>> {
self.load_candidate_entry(candidate_hash)
}
@@ -364,6 +365,7 @@ impl V1ReadBackend for TestStore {
fn load_candidate_entry_v1(
&self,
candidate_hash: &CandidateHash,
_candidate_index: CandidateIndex,
) -> SubsystemResult<Option<CandidateEntry>> {
self.load_candidate_entry(candidate_hash)
}
@@ -446,6 +448,15 @@ fn sign_approval(
key.sign(&ApprovalVote(candidate_hash).signing_payload(session_index)).into()
}
fn sign_approval_multiple_candidates(
key: Sr25519Keyring,
candidate_hashes: Vec<CandidateHash>,
session_index: SessionIndex,
) -> ValidatorSignature {
key.sign(&ApprovalVoteMultipleCandidates(&candidate_hashes).signing_payload(session_index))
.into()
}
type VirtualOverseer = test_helpers::TestSubsystemContextHandle<ApprovalVotingMessage>;
#[derive(Default)]
@@ -641,7 +652,12 @@ async fn check_and_import_approval(
overseer,
FromOrchestra::Communication {
msg: ApprovalVotingMessage::CheckAndImportApproval(
IndirectSignedApprovalVote { block_hash, candidate_index, validator, signature },
IndirectSignedApprovalVoteV2 {
block_hash,
candidate_indices: candidate_index.into(),
validator,
signature,
},
tx,
),
},
@@ -2014,6 +2030,91 @@ fn forkful_import_at_same_height_act_on_leaf() {
});
}
#[test]
fn test_signing_a_single_candidate_is_backwards_compatible() {
let session_index = 1;
let block_hash = Hash::repeat_byte(0x01);
let candidate_descriptors = (1..10)
.into_iter()
.map(|val| make_candidate(ParaId::from(val as u32), &block_hash))
.collect::<Vec<CandidateReceipt>>();
let candidate_hashes = candidate_descriptors
.iter()
.map(|candidate_descriptor| candidate_descriptor.hash())
.collect_vec();
let first_descriptor = candidate_descriptors.first().unwrap();
let candidate_hash = first_descriptor.hash();
let sig_a = sign_approval(Sr25519Keyring::Alice, candidate_hash, session_index);
let sig_b = sign_approval(Sr25519Keyring::Alice, candidate_hash, session_index);
assert!(DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking)
.check_signature(
&Sr25519Keyring::Alice.public().into(),
candidate_hash,
session_index,
&sig_a,
)
.is_ok());
assert!(DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking)
.check_signature(
&Sr25519Keyring::Alice.public().into(),
candidate_hash,
session_index,
&sig_b,
)
.is_ok());
let sig_c = sign_approval_multiple_candidates(
Sr25519Keyring::Alice,
vec![candidate_hash],
session_index,
);
assert!(DisputeStatement::Valid(
ValidDisputeStatementKind::ApprovalCheckingMultipleCandidates(vec![candidate_hash])
)
.check_signature(&Sr25519Keyring::Alice.public().into(), candidate_hash, session_index, &sig_c,)
.is_ok());
assert!(DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking)
.check_signature(
&Sr25519Keyring::Alice.public().into(),
candidate_hash,
session_index,
&sig_c,
)
.is_ok());
assert!(DisputeStatement::Valid(
ValidDisputeStatementKind::ApprovalCheckingMultipleCandidates(vec![candidate_hash])
)
.check_signature(&Sr25519Keyring::Alice.public().into(), candidate_hash, session_index, &sig_a,)
.is_ok());
let sig_all = sign_approval_multiple_candidates(
Sr25519Keyring::Alice,
candidate_hashes.clone(),
session_index,
);
assert!(DisputeStatement::Valid(
ValidDisputeStatementKind::ApprovalCheckingMultipleCandidates(candidate_hashes.clone())
)
.check_signature(
&Sr25519Keyring::Alice.public().into(),
*candidate_hashes.first().expect("test"),
session_index,
&sig_all,
)
.is_ok());
}
#[test]
fn import_checked_approval_updates_entries_and_schedules() {
let config = HarnessConfig::default();
@@ -2730,11 +2831,29 @@ async fn handle_double_assignment_import(
}
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::ApprovalVotingParams(_, sender))) => {
let _ = sender.send(Ok(ApprovalVotingParams {
max_approval_coalesce_count: 1,
}));
}
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::ApprovalDistribution(ApprovalDistributionMessage::DistributeApproval(_))
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::ApprovalVotingParams(_, sender))) => {
let _ = sender.send(Ok(ApprovalVotingParams {
max_approval_coalesce_count: 1,
}));
}
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::ApprovalDistribution(ApprovalDistributionMessage::DistributeApproval(_))
@@ -3469,3 +3588,455 @@ fn waits_until_approving_assignments_are_old_enough() {
virtual_overseer
});
}
#[test]
fn test_approval_is_sent_on_max_approval_coalesce_count() {
let assignment_criteria = Box::new(MockAssignmentCriteria(
|| {
let mut assignments = HashMap::new();
let _ = assignments.insert(
CoreIndex(0),
approval_db::v2::OurAssignment {
cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 })
.into(),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
}
.into(),
);
let assignments_cert =
garbage_assignment_cert_v2(AssignmentCertKindV2::RelayVRFModuloCompact {
core_bitfield: vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)]
.try_into()
.unwrap(),
});
let _ = assignments.insert(
CoreIndex(0),
approval_db::v2::OurAssignment {
cert: assignments_cert.clone(),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
}
.into(),
);
let _ = assignments.insert(
CoreIndex(1),
approval_db::v2::OurAssignment {
cert: assignments_cert.clone(),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
}
.into(),
);
assignments
},
|_| Ok(0),
));
let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build();
let store = config.backend();
test_harness(config, |test_harness| async move {
let TestHarness { mut virtual_overseer, clock, sync_oracle_handle: _sync_oracle_handle } =
test_harness;
assert_matches!(
overseer_recv(&mut virtual_overseer).await,
AllMessages::ChainApi(ChainApiMessage::FinalizedBlockNumber(rx)) => {
rx.send(Ok(0)).unwrap();
}
);
let block_hash = Hash::repeat_byte(0x01);
let candidate_commitments = CandidateCommitments::default();
let candidate_receipt1 = {
let mut receipt = dummy_candidate_receipt(block_hash);
receipt.descriptor.para_id = ParaId::from(1_u32);
receipt.commitments_hash = candidate_commitments.hash();
receipt
};
let candidate_hash1 = candidate_receipt1.hash();
let candidate_receipt2 = {
let mut receipt = dummy_candidate_receipt(block_hash);
receipt.descriptor.para_id = ParaId::from(2_u32);
receipt.commitments_hash = candidate_commitments.hash();
receipt
};
let slot = Slot::from(1);
let candidate_index1 = 0;
let candidate_index2 = 1;
let validators = vec![
Sr25519Keyring::Alice,
Sr25519Keyring::Bob,
Sr25519Keyring::Charlie,
Sr25519Keyring::Dave,
Sr25519Keyring::Eve,
];
let session_info = SessionInfo {
validator_groups: IndexedVec::<GroupIndex, Vec<ValidatorIndex>>::from(vec![
vec![ValidatorIndex(0), ValidatorIndex(1)],
vec![ValidatorIndex(2)],
vec![ValidatorIndex(3), ValidatorIndex(4)],
]),
..session_info(&validators)
};
let candidates = Some(vec![
(candidate_receipt1.clone(), CoreIndex(0), GroupIndex(0)),
(candidate_receipt2.clone(), CoreIndex(1), GroupIndex(1)),
]);
ChainBuilder::new()
.add_block(
block_hash,
ChainBuilder::GENESIS_HASH,
1,
BlockConfig {
slot,
candidates: candidates.clone(),
session_info: Some(session_info.clone()),
},
)
.build(&mut virtual_overseer)
.await;
assert!(!clock.inner.lock().current_wakeup_is(1));
clock.inner.lock().wakeup_all(1);
assert!(clock.inner.lock().current_wakeup_is(slot_to_tick(slot)));
clock.inner.lock().wakeup_all(slot_to_tick(slot));
futures_timer::Delay::new(Duration::from_millis(200)).await;
clock.inner.lock().wakeup_all(slot_to_tick(slot + 2));
assert_eq!(clock.inner.lock().wakeups.len(), 0);
futures_timer::Delay::new(Duration::from_millis(200)).await;
let candidate_entry = store.load_candidate_entry(&candidate_hash1).unwrap().unwrap();
let our_assignment =
candidate_entry.approval_entry(&block_hash).unwrap().our_assignment().unwrap();
assert!(our_assignment.triggered());
handle_approval_on_max_coalesce_count(
&mut virtual_overseer,
vec![candidate_index1, candidate_index2],
)
.await;
virtual_overseer
});
}
async fn handle_approval_on_max_coalesce_count(
virtual_overseer: &mut VirtualOverseer,
candidate_indicies: Vec<CandidateIndex>,
) {
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::ApprovalDistribution(ApprovalDistributionMessage::DistributeAssignment(
_,
c_indices,
)) => {
assert_eq!(TryInto::<CandidateBitfield>::try_into(candidate_indicies.clone()).unwrap(), c_indices);
}
);
for _ in &candidate_indicies {
recover_available_data(virtual_overseer).await;
fetch_validation_code(virtual_overseer).await;
}
for _ in &candidate_indicies {
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::CandidateValidation(CandidateValidationMessage::ValidateFromExhaustive{exec_kind, response_sender, ..}) if exec_kind == PvfExecKind::Approval => {
response_sender.send(Ok(ValidationResult::Valid(Default::default(), Default::default())))
.unwrap();
}
);
}
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::ApprovalVotingParams(_, sender))) => {
let _ = sender.send(Ok(ApprovalVotingParams {
max_approval_coalesce_count: 2,
}));
}
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::ApprovalVotingParams(_, sender))) => {
let _ = sender.send(Ok(ApprovalVotingParams {
max_approval_coalesce_count: 2,
}));
}
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::ApprovalDistribution(ApprovalDistributionMessage::DistributeApproval(vote)) => {
assert_eq!(TryInto::<CandidateBitfield>::try_into(candidate_indicies).unwrap(), vote.candidate_indices);
}
);
// Assert that there are no more messages being sent by the subsystem
assert!(overseer_recv(virtual_overseer).timeout(TIMEOUT / 2).await.is_none());
}
async fn handle_approval_on_max_wait_time(
virtual_overseer: &mut VirtualOverseer,
candidate_indicies: Vec<CandidateIndex>,
clock: Box<MockClock>,
) {
const TICK_NOW_BEGIN: u64 = 1;
const MAX_COALESCE_COUNT: u32 = 3;
clock.inner.lock().set_tick(TICK_NOW_BEGIN);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::ApprovalDistribution(ApprovalDistributionMessage::DistributeAssignment(
_,
c_indices,
)) => {
assert_eq!(TryInto::<CandidateBitfield>::try_into(candidate_indicies.clone()).unwrap(), c_indices);
}
);
for _ in &candidate_indicies {
recover_available_data(virtual_overseer).await;
fetch_validation_code(virtual_overseer).await;
}
for _ in &candidate_indicies {
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::CandidateValidation(CandidateValidationMessage::ValidateFromExhaustive{exec_kind, response_sender, ..}) if exec_kind == PvfExecKind::Approval => {
response_sender.send(Ok(ValidationResult::Valid(Default::default(), Default::default())))
.unwrap();
}
);
}
// First time we fetch the configuration when we are ready to approve the first candidate
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::ApprovalVotingParams(_, sender))) => {
let _ = sender.send(Ok(ApprovalVotingParams {
max_approval_coalesce_count: MAX_COALESCE_COUNT,
}));
}
);
// Second time we fetch the configuration when we are ready to approve the second candidate
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::ApprovalVotingParams(_, sender))) => {
let _ = sender.send(Ok(ApprovalVotingParams {
max_approval_coalesce_count: MAX_COALESCE_COUNT,
}));
}
);
assert!(overseer_recv(virtual_overseer).timeout(TIMEOUT / 2).await.is_none());
// Move the clock just before we should send the approval
clock
.inner
.lock()
.set_tick(MAX_APPROVAL_COALESCE_WAIT_TICKS as Tick + TICK_NOW_BEGIN - 1);
assert!(overseer_recv(virtual_overseer).timeout(TIMEOUT / 2).await.is_none());
// Move the clock tick, so we can trigger a force sending of the approvals
clock
.inner
.lock()
.set_tick(MAX_APPROVAL_COALESCE_WAIT_TICKS as Tick + TICK_NOW_BEGIN);
// Third time we fetch the configuration when timer expires and we are ready to sent the
// approval
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::RuntimeApi(RuntimeApiMessage::Request(_, RuntimeApiRequest::ApprovalVotingParams(_, sender))) => {
let _ = sender.send(Ok(ApprovalVotingParams {
max_approval_coalesce_count: 3,
}));
}
);
assert_matches!(
overseer_recv(virtual_overseer).await,
AllMessages::ApprovalDistribution(ApprovalDistributionMessage::DistributeApproval(vote)) => {
assert_eq!(TryInto::<CandidateBitfield>::try_into(candidate_indicies).unwrap(), vote.candidate_indices);
}
);
// Assert that there are no more messages being sent by the subsystem
assert!(overseer_recv(virtual_overseer).timeout(TIMEOUT / 2).await.is_none());
}
#[test]
fn test_approval_is_sent_on_max_approval_coalesce_wait() {
let assignment_criteria = Box::new(MockAssignmentCriteria(
|| {
let mut assignments = HashMap::new();
let _ = assignments.insert(
CoreIndex(0),
approval_db::v2::OurAssignment {
cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 })
.into(),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
}
.into(),
);
let assignments_cert =
garbage_assignment_cert_v2(AssignmentCertKindV2::RelayVRFModuloCompact {
core_bitfield: vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)]
.try_into()
.unwrap(),
});
let _ = assignments.insert(
CoreIndex(0),
approval_db::v2::OurAssignment {
cert: assignments_cert.clone(),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
}
.into(),
);
let _ = assignments.insert(
CoreIndex(1),
approval_db::v2::OurAssignment {
cert: assignments_cert.clone(),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
}
.into(),
);
assignments
},
|_| Ok(0),
));
let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build();
let store = config.backend();
test_harness(config, |test_harness| async move {
let TestHarness { mut virtual_overseer, clock, sync_oracle_handle: _sync_oracle_handle } =
test_harness;
assert_matches!(
overseer_recv(&mut virtual_overseer).await,
AllMessages::ChainApi(ChainApiMessage::FinalizedBlockNumber(rx)) => {
rx.send(Ok(0)).unwrap();
}
);
let block_hash = Hash::repeat_byte(0x01);
let candidate_commitments = CandidateCommitments::default();
let candidate_receipt1 = {
let mut receipt = dummy_candidate_receipt(block_hash);
receipt.descriptor.para_id = ParaId::from(1_u32);
receipt.commitments_hash = candidate_commitments.hash();
receipt
};
let candidate_hash1 = candidate_receipt1.hash();
let candidate_receipt2 = {
let mut receipt = dummy_candidate_receipt(block_hash);
receipt.descriptor.para_id = ParaId::from(2_u32);
receipt.commitments_hash = candidate_commitments.hash();
receipt
};
let slot = Slot::from(1);
let candidate_index1 = 0;
let candidate_index2 = 1;
let validators = vec![
Sr25519Keyring::Alice,
Sr25519Keyring::Bob,
Sr25519Keyring::Charlie,
Sr25519Keyring::Dave,
Sr25519Keyring::Eve,
];
let session_info = SessionInfo {
validator_groups: IndexedVec::<GroupIndex, Vec<ValidatorIndex>>::from(vec![
vec![ValidatorIndex(0), ValidatorIndex(1)],
vec![ValidatorIndex(2)],
vec![ValidatorIndex(3), ValidatorIndex(4)],
]),
..session_info(&validators)
};
let candidates = Some(vec![
(candidate_receipt1.clone(), CoreIndex(0), GroupIndex(0)),
(candidate_receipt2.clone(), CoreIndex(1), GroupIndex(1)),
]);
ChainBuilder::new()
.add_block(
block_hash,
ChainBuilder::GENESIS_HASH,
1,
BlockConfig {
slot,
candidates: candidates.clone(),
session_info: Some(session_info.clone()),
},
)
.build(&mut virtual_overseer)
.await;
assert!(!clock.inner.lock().current_wakeup_is(1));
clock.inner.lock().wakeup_all(1);
assert!(clock.inner.lock().current_wakeup_is(slot_to_tick(slot)));
clock.inner.lock().wakeup_all(slot_to_tick(slot));
futures_timer::Delay::new(Duration::from_millis(200)).await;
clock.inner.lock().wakeup_all(slot_to_tick(slot + 2));
assert_eq!(clock.inner.lock().wakeups.len(), 0);
futures_timer::Delay::new(Duration::from_millis(200)).await;
let candidate_entry = store.load_candidate_entry(&candidate_hash1).unwrap().unwrap();
let our_assignment =
candidate_entry.approval_entry(&block_hash).unwrap().our_assignment().unwrap();
assert!(our_assignment.triggered());
handle_approval_on_max_wait_time(
&mut virtual_overseer,
vec![candidate_index1, candidate_index2],
clock,
)
.await;
virtual_overseer
});
}
+164 -1
View File
@@ -16,14 +16,23 @@
//! Time utilities for approval voting.
use futures::prelude::*;
use futures::{
future::BoxFuture,
prelude::*,
stream::{FusedStream, FuturesUnordered},
Stream, StreamExt,
};
use polkadot_node_primitives::approval::v1::DelayTranche;
use sp_consensus_slots::Slot;
use std::{
collections::HashSet,
pin::Pin,
task::Poll,
time::{Duration, SystemTime},
};
use polkadot_primitives::{Hash, ValidatorIndex};
const TICK_DURATION_MILLIS: u64 = 500;
/// A base unit of time, starting from the Unix epoch, split into half-second intervals.
@@ -88,3 +97,157 @@ pub(crate) fn slot_number_to_tick(slot_duration_millis: u64, slot: Slot) -> Tick
let ticks_per_slot = slot_duration_millis / TICK_DURATION_MILLIS;
u64::from(slot) * ticks_per_slot
}
/// A list of delayed futures that gets triggered when the waiting time has expired and it is
/// time to sign the candidate.
/// We have a timer per relay-chain block.
#[derive(Default)]
pub struct DelayedApprovalTimer {
timers: FuturesUnordered<BoxFuture<'static, (Hash, ValidatorIndex)>>,
blocks: HashSet<Hash>,
}
impl DelayedApprovalTimer {
/// Starts a single timer per block hash
///
/// Guarantees that if a timer already exits for the give block hash,
/// no additional timer is started.
pub(crate) fn maybe_arm_timer(
&mut self,
wait_untill: Tick,
clock: &dyn Clock,
block_hash: Hash,
validator_index: ValidatorIndex,
) {
if self.blocks.insert(block_hash) {
let clock_wait = clock.wait(wait_untill);
self.timers.push(Box::pin(async move {
clock_wait.await;
(block_hash, validator_index)
}));
}
}
}
impl Stream for DelayedApprovalTimer {
type Item = (Hash, ValidatorIndex);
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let poll_result = self.timers.poll_next_unpin(cx);
match poll_result {
Poll::Ready(Some(result)) => {
self.blocks.remove(&result.0);
Poll::Ready(Some(result))
},
_ => poll_result,
}
}
}
impl FusedStream for DelayedApprovalTimer {
fn is_terminated(&self) -> bool {
self.timers.is_terminated()
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use futures::{executor::block_on, FutureExt, StreamExt};
use futures_timer::Delay;
use polkadot_primitives::{Hash, ValidatorIndex};
use crate::time::{Clock, SystemClock};
use super::DelayedApprovalTimer;
#[test]
fn test_select_empty_timer() {
block_on(async move {
let mut timer = DelayedApprovalTimer::default();
for _ in 1..10 {
let result = futures::select!(
_ = timer.select_next_some() => {
0
}
// Only this arm should fire
_ = Delay::new(Duration::from_millis(100)).fuse() => {
1
}
);
assert_eq!(result, 1);
}
});
}
#[test]
fn test_timer_functionality() {
block_on(async move {
let mut timer = DelayedApprovalTimer::default();
let test_hashes =
vec![Hash::repeat_byte(0x01), Hash::repeat_byte(0x02), Hash::repeat_byte(0x03)];
for (index, hash) in test_hashes.iter().enumerate() {
timer.maybe_arm_timer(
SystemClock.tick_now() + index as u64,
&SystemClock,
*hash,
ValidatorIndex::from(2),
);
timer.maybe_arm_timer(
SystemClock.tick_now() + index as u64,
&SystemClock,
*hash,
ValidatorIndex::from(2),
);
}
let timeout_hash = Hash::repeat_byte(0x02);
for i in 0..test_hashes.len() * 2 {
let result = futures::select!(
(hash, _) = timer.select_next_some() => {
hash
}
// Timers should fire only once, so for the rest of the iterations we should timeout through here.
_ = Delay::new(Duration::from_secs(2)).fuse() => {
timeout_hash
}
);
assert_eq!(test_hashes.get(i).cloned().unwrap_or(timeout_hash), result);
}
// Now check timer can be restarted if already fired
for (index, hash) in test_hashes.iter().enumerate() {
timer.maybe_arm_timer(
SystemClock.tick_now() + index as u64,
&SystemClock,
*hash,
ValidatorIndex::from(2),
);
timer.maybe_arm_timer(
SystemClock.tick_now() + index as u64,
&SystemClock,
*hash,
ValidatorIndex::from(2),
);
}
for i in 0..test_hashes.len() * 2 {
let result = futures::select!(
(hash, _) = timer.select_next_some() => {
hash
}
// Timers should fire only once, so for the rest of the iterations we should timeout through here.
_ = Delay::new(Duration::from_secs(2)).fuse() => {
timeout_hash
}
);
assert_eq!(test_hashes.get(i).cloned().unwrap_or(timeout_hash), result);
}
});
}
}