Approval Voting improvements (#2781)

* extract database from av-store itself

* generalize approval-voting over database type

* modes (without handling) and pruning old wakeups

* rework approval importing

* add our_approval_sig to ApprovalEntry

* import assignment

* guide updates for check-full-approval changes

* some aux functions

* send messages when becoming active.

* guide: network bridge sends view updates only when done syncing

* network bridge: send view updates only when done syncing

* tests for new network-bridge behavior

* add a test for updating approval entry with sig

* fix some warnings

* test load-all-blocks

* instantiate new parachains DB

* fix network-bridge empty view updates

* tweak

* fix wasm build, i think

* Update node/core/approval-voting/src/lib.rs

Co-authored-by: Andronik Ordian <write@reusable.software>

* add some versioning to parachains_db

* warnings

* fix merge changes

* remove versioning again

Co-authored-by: Andronik Ordian <write@reusable.software>
This commit is contained in:
Robert Habermeier
2021-04-01 19:33:52 +02:00
committed by GitHub
parent 01badafba6
commit 57b56770e0
20 changed files with 1593 additions and 701 deletions
@@ -406,6 +406,7 @@ mod tests {
tranches: Vec::new(),
assignments: Default::default(),
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
approved: false,
}.into();
@@ -452,6 +453,7 @@ mod tests {
],
assignments: bitvec![BitOrderLsb0, u8; 1; 10],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
approved: false,
}.into();
@@ -515,6 +517,7 @@ mod tests {
],
assignments: bitvec![BitOrderLsb0, u8; 1; 10],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
approved: false,
}.into();
@@ -582,6 +585,7 @@ mod tests {
tranches: Vec::new(),
assignments: bitvec![BitOrderLsb0, u8; 0; 5],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
approved: false,
}.into();
@@ -619,6 +623,7 @@ mod tests {
tranches: Vec::new(),
assignments: bitvec![BitOrderLsb0, u8; 0; 10],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
approved: false,
}.into();
@@ -657,6 +662,7 @@ mod tests {
tranches: Vec::new(),
assignments: bitvec![BitOrderLsb0, u8; 0; 10],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
approved: false,
}.into();
@@ -700,6 +706,7 @@ mod tests {
tranches: Vec::new(),
assignments: bitvec![BitOrderLsb0, u8; 0; n_validators],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
approved: false,
}.into();
@@ -765,6 +772,7 @@ mod tests {
tranches: Vec::new(),
assignments: bitvec![BitOrderLsb0, u8; 0; n_validators],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
approved: false,
}.into();
@@ -852,6 +860,7 @@ mod tests {
tranches: Vec::new(),
assignments: bitvec![BitOrderLsb0, u8; 0; n_validators],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
approved: false,
}.into();
@@ -20,14 +20,13 @@ use kvdb::{DBTransaction, KeyValueDB};
use polkadot_node_primitives::approval::{DelayTranche, AssignmentCert};
use polkadot_primitives::v1::{
ValidatorIndex, GroupIndex, CandidateReceipt, SessionIndex, CoreIndex,
BlockNumber, Hash, CandidateHash,
BlockNumber, Hash, CandidateHash, ValidatorSignature,
};
use sp_consensus_slots::Slot;
use parity_scale_codec::{Encode, Decode};
use std::collections::{BTreeMap, HashMap};
use std::collections::hash_map::Entry;
use std::sync::Arc;
use bitvec::{vec::BitVec, order::Lsb0 as BitOrderLsb0};
#[cfg(test)]
@@ -40,11 +39,15 @@ pub struct Tick(u64);
pub type Bitfield = BitVec<BitOrderLsb0, u8>;
const NUM_COLUMNS: u32 = 1;
const DATA_COL: u32 = 0;
const STORED_BLOCKS_KEY: &[u8] = b"Approvals_StoredBlocks";
/// The database config.
#[derive(Debug, Clone, Copy)]
pub struct Config {
/// The column family in the database where data is stored.
pub col_data: u32,
}
/// Details pertaining to our assignment on a block.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct OurAssignment {
@@ -71,6 +74,7 @@ pub struct ApprovalEntry {
pub tranches: Vec<TrancheEntry>,
pub backing_group: GroupIndex,
pub our_assignment: Option<OurAssignment>,
pub our_approval_sig: Option<ValidatorSignature>,
// `n_validators` bits.
pub assignments: Bitfield,
pub approved: bool,
@@ -109,33 +113,6 @@ pub struct BlockEntry {
pub children: Vec<Hash>,
}
/// Clear the given directory and create a RocksDB instance there.
pub fn clear_and_recreate(path: &std::path::Path, cache_size: usize)
-> std::io::Result<Arc<dyn KeyValueDB>>
{
use kvdb_rocksdb::{DatabaseConfig, Database as RocksDB};
tracing::info!("Recreating approval-checking DB at {:?}", path);
if let Err(e) = std::fs::remove_dir_all(path) {
if e.kind() != std::io::ErrorKind::NotFound {
return Err(e);
}
}
std::fs::create_dir_all(path)?;
let mut db_config = DatabaseConfig::with_columns(NUM_COLUMNS);
db_config.memory_budget.insert(DATA_COL, cache_size);
let path = path.to_str().ok_or_else(|| std::io::Error::new(
std::io::ErrorKind::Other,
format!("Non-UTF-8 database path {:?}", path),
))?;
Ok(Arc::new(RocksDB::open(&db_config, path)?))
}
/// A range from earliest..last block number stored within the DB.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct StoredBlockRange(BlockNumber, BlockNumber);
@@ -168,12 +145,13 @@ pub type Result<T> = std::result::Result<T, Error>;
/// pruning any competing branches at the same height.
pub(crate) fn canonicalize(
store: &dyn KeyValueDB,
config: &Config,
canon_number: BlockNumber,
canon_hash: Hash,
)
-> Result<()>
{
let range = match load_stored_blocks(store)? {
let range = match load_stored_blocks(store, config)? {
None => return Ok(()),
Some(range) => if range.0 >= canon_number {
return Ok(())
@@ -197,17 +175,17 @@ pub(crate) fn canonicalize(
transaction: &mut DBTransaction,
visited_candidates: &mut HashMap<CandidateHash, CandidateEntry>,
| -> Result<Vec<Hash>> {
let block_entry = match load_block_entry(store, &block_hash)? {
let block_entry = match load_block_entry(store, config, &block_hash)? {
None => return Ok(Vec::new()),
Some(b) => b,
};
transaction.delete(DATA_COL, &block_entry_key(&block_hash)[..]);
transaction.delete(config.col_data, &block_entry_key(&block_hash)[..]);
for &(_, ref candidate_hash) in &block_entry.candidates {
let candidate = match visited_candidates.entry(*candidate_hash) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => {
e.insert(match load_candidate_entry(store, candidate_hash)? {
e.insert(match load_candidate_entry(store, config, candidate_hash)? {
None => continue, // Should not happen except for corrupt DB
Some(c) => c,
})
@@ -222,8 +200,8 @@ pub(crate) fn canonicalize(
// First visit everything before the height.
for i in range.0..canon_number {
let at_height = load_blocks_at_height(store, i)?;
transaction.delete(DATA_COL, &blocks_at_height_key(i)[..]);
let at_height = load_blocks_at_height(store, config, i)?;
transaction.delete(config.col_data, &blocks_at_height_key(i)[..]);
for b in at_height {
let _ = visit_and_remove_block_entry(
@@ -236,8 +214,8 @@ pub(crate) fn canonicalize(
// Then visit everything at the height.
let pruned_branches = {
let at_height = load_blocks_at_height(store, canon_number)?;
transaction.delete(DATA_COL, &blocks_at_height_key(canon_number));
let at_height = load_blocks_at_height(store, config, canon_number)?;
transaction.delete(config.col_data, &blocks_at_height_key(canon_number));
// Note that while there may be branches descending from blocks at earlier heights,
// we have already covered them by removing everything at earlier heights.
@@ -274,7 +252,7 @@ pub(crate) fn canonicalize(
// visit the at-height key for this deleted block's height.
let at_height = match visited_heights.entry(height) {
Entry::Occupied(e) => e.into_mut(),
Entry::Vacant(e) => e.insert(load_blocks_at_height(store, height)?),
Entry::Vacant(e) => e.insert(load_blocks_at_height(store, config, height)?),
};
if let Some(i) = at_height.iter().position(|x| x == &next_child) {
@@ -286,10 +264,10 @@ pub(crate) fn canonicalize(
// Update all `CandidateEntry`s, deleting all those which now have empty `block_assignments`.
for (candidate_hash, candidate) in visited_candidates {
if candidate.block_assignments.is_empty() {
transaction.delete(DATA_COL, &candidate_entry_key(&candidate_hash)[..]);
transaction.delete(config.col_data, &candidate_entry_key(&candidate_hash)[..]);
} else {
transaction.put_vec(
DATA_COL,
config.col_data,
&candidate_entry_key(&candidate_hash)[..],
candidate.encode(),
);
@@ -299,9 +277,9 @@ pub(crate) fn canonicalize(
// Update all blocks-at-height keys, deleting all those which now have empty `block_assignments`.
for (h, at) in visited_heights {
if at.is_empty() {
transaction.delete(DATA_COL, &blocks_at_height_key(h)[..]);
transaction.delete(config.col_data, &blocks_at_height_key(h)[..]);
} else {
transaction.put_vec(DATA_COL, &blocks_at_height_key(h), at.encode());
transaction.put_vec(config.col_data, &blocks_at_height_key(h), at.encode());
}
}
@@ -312,16 +290,16 @@ pub(crate) fn canonicalize(
std::cmp::max(range.1, canon_number + 2),
).encode();
transaction.put_vec(DATA_COL, &STORED_BLOCKS_KEY[..], new_range);
transaction.put_vec(config.col_data, &STORED_BLOCKS_KEY[..], new_range);
// Update the values on-disk.
store.write(transaction).map_err(Into::into)
}
fn load_decode<D: Decode>(store: &dyn KeyValueDB, key: &[u8])
fn load_decode<D: Decode>(store: &dyn KeyValueDB, col_data: u32, key: &[u8])
-> Result<Option<D>>
{
match store.get(DATA_COL, key)? {
match store.get(col_data, key)? {
None => Ok(None),
Some(raw) => D::decode(&mut &raw[..])
.map(Some)
@@ -350,6 +328,7 @@ pub(crate) struct NewCandidateInfo {
/// no information about new candidates will be referred to by this function.
pub(crate) fn add_block_entry(
store: &dyn KeyValueDB,
config: &Config,
entry: BlockEntry,
n_validators: usize,
candidate_info: impl Fn(&CandidateHash) -> Option<NewCandidateInfo>,
@@ -361,7 +340,7 @@ pub(crate) fn add_block_entry(
// Update the stored block range.
{
let new_range = match load_stored_blocks(store)? {
let new_range = match load_stored_blocks(store, config)? {
None => Some(StoredBlockRange(number, number + 1)),
Some(range) => if range.1 <= number {
Some(StoredBlockRange(range.0, number + 1))
@@ -370,19 +349,19 @@ pub(crate) fn add_block_entry(
}
};
new_range.map(|n| transaction.put_vec(DATA_COL, &STORED_BLOCKS_KEY[..], n.encode()))
new_range.map(|n| transaction.put_vec(config.col_data, &STORED_BLOCKS_KEY[..], n.encode()))
};
// Update the blocks at height meta key.
{
let mut blocks_at_height = load_blocks_at_height(store, number)?;
let mut blocks_at_height = load_blocks_at_height(store, config, number)?;
if blocks_at_height.contains(&entry.block_hash) {
// seems we already have a block entry for this block. nothing to do here.
return Ok(Vec::new())
}
blocks_at_height.push(entry.block_hash);
transaction.put_vec(DATA_COL, &blocks_at_height_key(number)[..], blocks_at_height.encode())
transaction.put_vec(config.col_data, &blocks_at_height_key(number)[..], blocks_at_height.encode())
};
let mut candidate_entries = Vec::with_capacity(entry.candidates.len());
@@ -399,7 +378,7 @@ pub(crate) fn add_block_entry(
Some(info) => info,
};
let mut candidate_entry = load_candidate_entry(store, &candidate_hash)?
let mut candidate_entry = load_candidate_entry(store, config, &candidate_hash)?
.unwrap_or_else(move || CandidateEntry {
candidate,
session,
@@ -413,13 +392,14 @@ pub(crate) fn add_block_entry(
tranches: Vec::new(),
backing_group,
our_assignment,
our_approval_sig: None,
assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; n_validators],
approved: false,
}
);
transaction.put_vec(
DATA_COL,
config.col_data,
&candidate_entry_key(&candidate_hash)[..],
candidate_entry.encode(),
);
@@ -429,13 +409,13 @@ pub(crate) fn add_block_entry(
};
// Update the child index for the parent.
load_block_entry(store, &parent_hash)?.map(|mut e| {
load_block_entry(store, config, &parent_hash)?.map(|mut e| {
e.children.push(entry.block_hash);
transaction.put_vec(DATA_COL, &block_entry_key(&parent_hash)[..], e.encode())
transaction.put_vec(config.col_data, &block_entry_key(&parent_hash)[..], e.encode())
});
// Put the new block entry in.
transaction.put_vec(DATA_COL, &block_entry_key(&entry.block_hash)[..], entry.encode());
transaction.put_vec(config.col_data, &block_entry_key(&entry.block_hash)[..], entry.encode());
store.write(transaction)?;
Ok(candidate_entries)
@@ -445,6 +425,7 @@ pub(crate) fn add_block_entry(
/// chain.
pub fn force_approve(
store: &dyn KeyValueDB,
db_config: Config,
chain_head: Hash,
up_to: BlockNumber,
) -> Result<()> {
@@ -456,11 +437,11 @@ pub fn force_approve(
let mut cur_hash = chain_head;
let mut state = State::WalkTo;
let mut tx = Transaction::default();
let mut tx = Transaction::new(db_config);
// iterate back to the `up_to` block, and then iterate backwards until all blocks
// are updated.
while let Some(mut entry) = load_block_entry(store, &cur_hash)? {
while let Some(mut entry) = load_block_entry(store, &db_config, &cur_hash)? {
if entry.block_number <= up_to {
state = State::Approving;
@@ -480,15 +461,35 @@ pub fn force_approve(
tx.write(store)
}
/// Return all blocks which have entries in the DB, ascending, by height.
pub(crate) fn load_all_blocks(store: &dyn KeyValueDB, config: &Config) -> Result<Vec<Hash>> {
let stored_blocks = load_stored_blocks(store, config)?;
let mut hashes = Vec::new();
for height in stored_blocks.into_iter().flat_map(|s| s.0..s.1) {
hashes.extend(load_blocks_at_height(store, config, height)?);
}
Ok(hashes)
}
// An atomic transaction of multiple candidate or block entries.
#[derive(Default)]
#[must_use = "Transactions do nothing unless written to a DB"]
pub struct Transaction {
block_entries: HashMap<Hash, BlockEntry>,
candidate_entries: HashMap<CandidateHash, CandidateEntry>,
config: Config,
}
impl Transaction {
pub(crate) fn new(config: Config) -> Self {
Transaction {
block_entries: HashMap::default(),
candidate_entries: HashMap::default(),
config,
}
}
/// Put a block entry in the transaction, overwriting any other with the
/// same hash.
pub(crate) fn put_block_entry(&mut self, entry: BlockEntry) {
@@ -519,14 +520,14 @@ impl Transaction {
let k = block_entry_key(&hash);
let v = entry.encode();
db_transaction.put_vec(DATA_COL, &k, v);
db_transaction.put_vec(self.config.col_data, &k, v);
}
for (hash, entry) in self.candidate_entries {
let k = candidate_entry_key(&hash);
let v = entry.encode();
db_transaction.put_vec(DATA_COL, &k, v);
db_transaction.put_vec(self.config.col_data, &k, v);
}
db.write(db_transaction).map_err(Into::into)
@@ -534,31 +535,39 @@ impl Transaction {
}
/// Load the stored-blocks key from the state.
fn load_stored_blocks(store: &dyn KeyValueDB)
fn load_stored_blocks(store: &dyn KeyValueDB, config: &Config)
-> Result<Option<StoredBlockRange>>
{
load_decode(store, STORED_BLOCKS_KEY)
load_decode(store, config.col_data, STORED_BLOCKS_KEY)
}
/// Load a blocks-at-height entry for a given block number.
pub(crate) fn load_blocks_at_height(store: &dyn KeyValueDB, block_number: BlockNumber)
pub(crate) fn load_blocks_at_height(
store: &dyn KeyValueDB,
config: &Config,
block_number: BlockNumber,
)
-> Result<Vec<Hash>> {
load_decode(store, &blocks_at_height_key(block_number))
load_decode(store, config.col_data, &blocks_at_height_key(block_number))
.map(|x| x.unwrap_or_default())
}
/// Load a block entry from the aux store.
pub(crate) fn load_block_entry(store: &dyn KeyValueDB, block_hash: &Hash)
pub(crate) fn load_block_entry(store: &dyn KeyValueDB, config: &Config, block_hash: &Hash)
-> Result<Option<BlockEntry>>
{
load_decode(store, &block_entry_key(block_hash))
load_decode(store, config.col_data, &block_entry_key(block_hash))
}
/// Load a candidate entry from the aux store.
pub(crate) fn load_candidate_entry(store: &dyn KeyValueDB, candidate_hash: &CandidateHash)
pub(crate) fn load_candidate_entry(
store: &dyn KeyValueDB,
config: &Config,
candidate_hash: &CandidateHash,
)
-> Result<Option<CandidateEntry>>
{
load_decode(store, &candidate_entry_key(candidate_hash))
load_decode(store, config.col_data, &candidate_entry_key(candidate_hash))
}
/// The key a given block entry is stored under.
@@ -19,6 +19,13 @@
use super::*;
use polkadot_primitives::v1::Id as ParaId;
const DATA_COL: u32 = 0;
const NUM_COLUMNS: u32 = 1;
const TEST_CONFIG: Config = Config {
col_data: DATA_COL,
};
pub(crate) fn write_stored_blocks(tx: &mut DBTransaction, range: StoredBlockRange) {
tx.put_vec(
DATA_COL,
@@ -85,7 +92,7 @@ fn make_candidate(para_id: ParaId, relay_parent: Hash) -> CandidateReceipt {
#[test]
fn read_write() {
let store = kvdb_memorydb::create(1);
let store = kvdb_memorydb::create(NUM_COLUMNS);
let hash_a = Hash::repeat_byte(1);
let hash_b = Hash::repeat_byte(2);
@@ -109,6 +116,7 @@ fn read_write() {
tranches: Vec::new(),
backing_group: GroupIndex(1),
our_assignment: None,
our_approval_sig: None,
assignments: Default::default(),
approved: false,
})
@@ -125,10 +133,13 @@ fn read_write() {
store.write(tx).unwrap();
assert_eq!(load_stored_blocks(&store).unwrap(), Some(range));
assert_eq!(load_blocks_at_height(&store, 1).unwrap(), at_height);
assert_eq!(load_block_entry(&store, &hash_a).unwrap(), Some(block_entry));
assert_eq!(load_candidate_entry(&store, &candidate_hash).unwrap(), Some(candidate_entry));
assert_eq!(load_stored_blocks(&store, &TEST_CONFIG).unwrap(), Some(range));
assert_eq!(load_blocks_at_height(&store, &TEST_CONFIG, 1).unwrap(), at_height);
assert_eq!(load_block_entry(&store, &TEST_CONFIG, &hash_a).unwrap(), Some(block_entry));
assert_eq!(
load_candidate_entry(&store, &TEST_CONFIG, &candidate_hash).unwrap(),
Some(candidate_entry),
);
let delete_keys = vec![
STORED_BLOCKS_KEY.to_vec(),
@@ -144,15 +155,15 @@ fn read_write() {
store.write(tx).unwrap();
assert!(load_stored_blocks(&store).unwrap().is_none());
assert!(load_blocks_at_height(&store, 1).unwrap().is_empty());
assert!(load_block_entry(&store, &hash_a).unwrap().is_none());
assert!(load_candidate_entry(&store, &candidate_hash).unwrap().is_none());
assert!(load_stored_blocks(&store, &TEST_CONFIG).unwrap().is_none());
assert!(load_blocks_at_height(&store, &TEST_CONFIG, 1).unwrap().is_empty());
assert!(load_block_entry(&store, &TEST_CONFIG, &hash_a).unwrap().is_none());
assert!(load_candidate_entry(&store, &TEST_CONFIG, &candidate_hash).unwrap().is_none());
}
#[test]
fn add_block_entry_works() {
let store = kvdb_memorydb::create(1);
let store = kvdb_memorydb::create(NUM_COLUMNS);
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
@@ -188,6 +199,7 @@ fn add_block_entry_works() {
add_block_entry(
&store,
&TEST_CONFIG,
block_entry_a.clone(),
n_validators,
|h| new_candidate_info.get(h).map(|x| x.clone()),
@@ -201,24 +213,27 @@ fn add_block_entry_works() {
add_block_entry(
&store,
&TEST_CONFIG,
block_entry_b.clone(),
n_validators,
|h| new_candidate_info.get(h).map(|x| x.clone()),
).unwrap();
assert_eq!(load_block_entry(&store, &block_hash_a).unwrap(), Some(block_entry_a));
assert_eq!(load_block_entry(&store, &block_hash_b).unwrap(), Some(block_entry_b));
assert_eq!(load_block_entry(&store, &TEST_CONFIG, &block_hash_a).unwrap(), Some(block_entry_a));
assert_eq!(load_block_entry(&store, &TEST_CONFIG, &block_hash_b).unwrap(), Some(block_entry_b));
let candidate_entry_a = load_candidate_entry(&store, &candidate_hash_a).unwrap().unwrap();
let candidate_entry_a = load_candidate_entry(&store, &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, &candidate_hash_b).unwrap().unwrap();
let candidate_entry_b = load_candidate_entry(&store, &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 store = kvdb_memorydb::create(1);
let store = kvdb_memorydb::create(NUM_COLUMNS);
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
@@ -242,6 +257,7 @@ fn add_block_entry_adds_child() {
add_block_entry(
&store,
&TEST_CONFIG,
block_entry_a.clone(),
n_validators,
|_| None,
@@ -249,6 +265,7 @@ fn add_block_entry_adds_child() {
add_block_entry(
&store,
&TEST_CONFIG,
block_entry_b.clone(),
n_validators,
|_| None,
@@ -256,13 +273,13 @@ fn add_block_entry_adds_child() {
block_entry_a.children.push(block_hash_b);
assert_eq!(load_block_entry(&store, &block_hash_a).unwrap(), Some(block_entry_a));
assert_eq!(load_block_entry(&store, &block_hash_b).unwrap(), Some(block_entry_b));
assert_eq!(load_block_entry(&store, &TEST_CONFIG, &block_hash_a).unwrap(), Some(block_entry_a));
assert_eq!(load_block_entry(&store, &TEST_CONFIG, &block_hash_b).unwrap(), Some(block_entry_b));
}
#[test]
fn canonicalize_works() {
let store = kvdb_memorydb::create(1);
let store = kvdb_memorydb::create(NUM_COLUMNS);
// -> B1 -> C1 -> D1
// A -> B2 -> C2 -> D2
@@ -377,6 +394,7 @@ fn canonicalize_works() {
for block_entry in blocks {
add_block_entry(
&store,
&TEST_CONFIG,
block_entry,
n_validators,
|h| candidate_info.get(h).map(|x| x.clone()),
@@ -387,11 +405,11 @@ fn canonicalize_works() {
for (c_hash, in_blocks) in expected {
let (entry, in_blocks) = match in_blocks {
None => {
assert!(load_candidate_entry(&store, &c_hash).unwrap().is_none());
assert!(load_candidate_entry(&store, &TEST_CONFIG, &c_hash).unwrap().is_none());
continue
}
Some(i) => (
load_candidate_entry(&store, &c_hash).unwrap().unwrap(),
load_candidate_entry(&store, &TEST_CONFIG, &c_hash).unwrap().unwrap(),
i,
),
};
@@ -408,11 +426,11 @@ fn canonicalize_works() {
for (hash, with_candidates) in expected {
let (entry, with_candidates) = match with_candidates {
None => {
assert!(load_block_entry(&store, &hash).unwrap().is_none());
assert!(load_block_entry(&store, &TEST_CONFIG, &hash).unwrap().is_none());
continue
}
Some(i) => (
load_block_entry(&store, &hash).unwrap().unwrap(),
load_block_entry(&store, &TEST_CONFIG, &hash).unwrap().unwrap(),
i,
),
};
@@ -443,9 +461,9 @@ fn canonicalize_works() {
(block_hash_d2, Some(vec![cand_hash_5])),
]);
canonicalize(&store, 3, block_hash_c1).unwrap();
canonicalize(&store, &TEST_CONFIG, 3, block_hash_c1).unwrap();
assert_eq!(load_stored_blocks(&store).unwrap().unwrap(), StoredBlockRange(4, 5));
assert_eq!(load_stored_blocks(&store, &TEST_CONFIG).unwrap().unwrap(), StoredBlockRange(4, 5));
check_candidates_in_store(vec![
(cand_hash_1, None),
@@ -468,7 +486,7 @@ fn canonicalize_works() {
#[test]
fn force_approve_works() {
let store = kvdb_memorydb::create(1);
let store = kvdb_memorydb::create(NUM_COLUMNS);
let n_validators = 10;
let mut tx = DBTransaction::new();
@@ -509,16 +527,101 @@ fn force_approve_works() {
for block_entry in blocks {
add_block_entry(
&store,
&TEST_CONFIG,
block_entry,
n_validators,
|h| candidate_info.get(h).map(|x| x.clone()),
).unwrap();
}
force_approve(&store, block_hash_d, 2).unwrap();
force_approve(&store, TEST_CONFIG, block_hash_d, 2).unwrap();
assert!(load_block_entry(&store, &block_hash_a).unwrap().unwrap().approved_bitfield.all());
assert!(load_block_entry(&store, &block_hash_b).unwrap().unwrap().approved_bitfield.all());
assert!(load_block_entry(&store, &block_hash_c).unwrap().unwrap().approved_bitfield.not_any());
assert!(load_block_entry(&store, &block_hash_d).unwrap().unwrap().approved_bitfield.not_any());
assert!(load_block_entry(
&store,
&TEST_CONFIG,
&block_hash_a,
).unwrap().unwrap().approved_bitfield.all());
assert!(load_block_entry(
&store,
&TEST_CONFIG,
&block_hash_b,
).unwrap().unwrap().approved_bitfield.all());
assert!(load_block_entry(
&store,
&TEST_CONFIG,
&block_hash_c,
).unwrap().unwrap().approved_bitfield.not_any());
assert!(load_block_entry(
&store,
&TEST_CONFIG,
&block_hash_d,
).unwrap().unwrap().approved_bitfield.not_any());
}
#[test]
fn load_all_blocks_works() {
let store = kvdb_memorydb::create(NUM_COLUMNS);
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;
add_block_entry(
&store,
&TEST_CONFIG,
block_entry_a.clone(),
n_validators,
|_| None
).unwrap();
// add C before B to test sorting.
add_block_entry(
&store,
&TEST_CONFIG,
block_entry_c.clone(),
n_validators,
|_| None
).unwrap();
add_block_entry(
&store,
&TEST_CONFIG,
block_entry_b.clone(),
n_validators,
|_| None
).unwrap();
assert_eq!(
load_all_blocks(
&store,
&TEST_CONFIG
).unwrap(),
vec![block_hash_a, block_hash_b, block_hash_c],
)
}
@@ -53,7 +53,7 @@ use bitvec::order::Lsb0 as BitOrderLsb0;
use std::collections::HashMap;
use std::convert::TryFrom;
use crate::approval_db;
use crate::approval_db::{self, v1::Config as DatabaseConfig};
use crate::persisted_entries::CandidateEntry;
use crate::criteria::{AssignmentCriteria, OurAssignment};
use crate::time::{slot_number_to_tick, Tick};
@@ -561,6 +561,7 @@ pub(crate) async fn handle_new_head(
ctx: &mut impl SubsystemContext,
state: &mut State<impl DBReader>,
db_writer: &dyn KeyValueDB,
db_config: DatabaseConfig,
head: Hash,
finalized_number: &Option<BlockNumber>,
) -> SubsystemResult<Vec<BlockImportedCandidates>> {
@@ -737,7 +738,7 @@ pub(crate) async fn handle_new_head(
"Enacting force-approve",
);
approval_db::v1::force_approve(db_writer, block_hash, up_to)
approval_db::v1::force_approve(db_writer, db_config, block_hash, up_to)
.map_err(|e| SubsystemError::with_origin("approval-voting", e))?;
}
@@ -750,6 +751,7 @@ pub(crate) async fn handle_new_head(
let candidate_entries = approval_db::v1::add_block_entry(
db_writer,
&db_config,
block_entry,
n_validators,
|candidate_hash| {
@@ -815,6 +817,13 @@ mod tests {
use crate::{criteria, BlockEntry};
const DATA_COL: u32 = 0;
const NUM_COLUMNS: u32 = 1;
const TEST_CONFIG: DatabaseConfig = DatabaseConfig {
col_data: DATA_COL,
};
#[derive(Default)]
struct TestDB {
block_entries: HashMap<Hash, BlockEntry>,
@@ -835,6 +844,14 @@ mod tests {
) -> SubsystemResult<Option<CandidateEntry>> {
Ok(self.candidate_entries.get(candidate_hash).map(|c| c.clone()))
}
fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>> {
let mut hashes: Vec<_> = self.block_entries.keys().cloned().collect();
hashes.sort_by_key(|k| self.block_entries.get(k).unwrap().block_number());
Ok(hashes)
}
}
#[derive(Default)]
@@ -1876,7 +1893,7 @@ mod tests {
}.into(),
);
let db_writer = kvdb_memorydb::create(1);
let db_writer = kvdb_memorydb::create(NUM_COLUMNS);
let test_fut = {
Box::pin(async move {
@@ -1884,6 +1901,7 @@ mod tests {
&mut ctx,
&mut state,
&db_writer,
TEST_CONFIG,
hash,
&Some(1),
).await.unwrap();
@@ -1895,7 +1913,11 @@ mod tests {
assert_eq!(candidates[1].1.approvals().len(), 6);
// the first candidate should be insta-approved
// the second should not
let entry: BlockEntry = crate::approval_db::v1::load_block_entry(&db_writer, &hash)
let entry: BlockEntry = crate::approval_db::v1::load_block_entry(
&db_writer,
&TEST_CONFIG,
&hash,
)
.unwrap()
.unwrap()
.into();
+430 -166
View File
@@ -43,10 +43,12 @@ use polkadot_primitives::v1::{
use polkadot_node_primitives::{ValidationResult, PoV};
use polkadot_node_primitives::approval::{
IndirectAssignmentCert, IndirectSignedApprovalVote, ApprovalVote, DelayTranche,
BlockApprovalMeta,
};
use polkadot_node_jaeger as jaeger;
use parity_scale_codec::Encode;
use sc_keystore::LocalKeystore;
use sp_consensus::SyncOracle;
use sp_consensus_slots::Slot;
use sp_runtime::traits::AppVerify;
use sp_application_crypto::Pair;
@@ -56,7 +58,7 @@ use futures::prelude::*;
use futures::future::RemoteHandle;
use futures::channel::{mpsc, oneshot};
use std::collections::{BTreeMap, HashMap};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::collections::btree_map::Entry;
use std::sync::Arc;
@@ -72,6 +74,8 @@ mod import;
mod time;
mod persisted_entries;
use crate::approval_db::v1::Config as DatabaseConfig;
#[cfg(test)]
mod tests;
@@ -79,25 +83,39 @@ const APPROVAL_SESSIONS: SessionIndex = 6;
const LOG_TARGET: &str = "parachain::approval-voting";
/// Configuration for the approval voting subsystem
#[derive(Debug, Clone)]
pub struct Config {
/// The path where the approval-voting DB should be kept. This directory is completely removed when starting
/// the service.
pub path: std::path::PathBuf,
/// The cache size, in bytes, to spend on approval checking metadata.
pub cache_size: Option<usize>,
/// The column family in the DB where approval-voting data is stored.
pub col_data: u32,
/// The slot duration of the consensus algorithm, in milliseconds. Should be evenly
/// divisible by 500.
pub slot_duration_millis: u64,
}
// The mode of the approval voting subsystem. It should start in a `Syncing` mode when it first
// starts, and then once it's reached the head of the chain it should move into the `Active` mode.
//
// In `Active` mode, the node is an active participant in the approvals protocol. When syncing,
// the node follows the new incoming blocks and finalized number, but does not yet participate.
//
// When transitioning from `Syncing` to `Active`, the node notifies the `ApprovalDistribution`
// subsystem of all unfinalized blocks and the candidates included within them, as well as all
// votes that the local node itself has cast on candidates within those blocks.
enum Mode {
Active,
Syncing(Box<dyn SyncOracle + Send>),
}
/// The approval voting subsystem.
pub struct ApprovalVotingSubsystem {
/// LocalKeystore is needed for assignment keys, but not necessarily approval keys.
///
/// We do a lot of VRF signing and need the keys to have low latency.
keystore: Arc<LocalKeystore>,
db_config: DatabaseConfig,
slot_duration_millis: u64,
db: Arc<dyn KeyValueDB>,
mode: Mode,
metrics: Metrics,
}
@@ -239,27 +257,24 @@ impl metrics::Metrics for Metrics {
}
impl ApprovalVotingSubsystem {
/// Create a new approval voting subsystem with the given keystore and config,
/// which creates a DB at the given path. This function will delete the directory
/// at the given path if it already exists.
/// Create a new approval voting subsystem with the given keystore, config, and database.
pub fn with_config(
config: Config,
db: Arc<dyn KeyValueDB>,
keystore: Arc<LocalKeystore>,
sync_oracle: Box<dyn SyncOracle + Send>,
metrics: Metrics,
) -> std::io::Result<Self> {
const DEFAULT_CACHE_SIZE: usize = 100 * 1024 * 1024; // 100MiB default should be fine unless finality stalls.
let db = approval_db::v1::clear_and_recreate(
&config.path,
config.cache_size.unwrap_or(DEFAULT_CACHE_SIZE),
)?;
Ok(ApprovalVotingSubsystem {
) -> Self {
ApprovalVotingSubsystem {
keystore,
slot_duration_millis: config.slot_duration_millis,
db,
db_config: DatabaseConfig {
col_data: config.col_data,
},
mode: Mode::Syncing(sync_oracle),
metrics,
})
}
}
}
@@ -305,6 +320,7 @@ struct Wakeups {
// Tick -> [(Relay Block, Candidate Hash)]
wakeups: BTreeMap<Tick, Vec<(Hash, CandidateHash)>>,
reverse_wakeups: HashMap<(Hash, CandidateHash), Tick>,
block_numbers: BTreeMap<BlockNumber, HashSet<Hash>>,
}
impl Wakeups {
@@ -313,9 +329,19 @@ impl Wakeups {
self.wakeups.keys().next().map(|t| *t)
}
fn note_block(&mut self, block_hash: Hash, block_number: BlockNumber) {
self.block_numbers.entry(block_number).or_default().insert(block_hash);
}
// Schedules a wakeup at the given tick. no-op if there is already an earlier or equal wake-up
// for these values. replaces any later wakeup.
fn schedule(&mut self, block_hash: Hash, candidate_hash: CandidateHash, tick: Tick) {
fn schedule(
&mut self,
block_hash: Hash,
block_number: BlockNumber,
candidate_hash: CandidateHash,
tick: Tick,
) {
if let Some(prev) = self.reverse_wakeups.get(&(block_hash, candidate_hash)) {
if prev <= &tick { return }
@@ -331,12 +357,42 @@ impl Wakeups {
let _ = entry.remove_entry();
}
}
} else {
self.note_block(block_hash, block_number);
}
self.reverse_wakeups.insert((block_hash, candidate_hash), tick);
self.wakeups.entry(tick).or_default().push((block_hash, candidate_hash));
}
fn prune_finalized_wakeups(&mut self, finalized_number: BlockNumber) {
let after = self.block_numbers.split_off(&(finalized_number + 1));
let pruned_blocks: HashSet<_> = std::mem::replace(&mut self.block_numbers, after)
.into_iter()
.flat_map(|(_number, hashes)| hashes)
.collect();
let mut pruned_wakeups = BTreeMap::new();
self.reverse_wakeups.retain(|&(ref h, ref c_h), tick| {
let live = !pruned_blocks.contains(h);
if !live {
pruned_wakeups.entry(*tick)
.or_insert_with(HashSet::new)
.insert((*h, *c_h));
}
live
});
for (tick, pruned) in pruned_wakeups {
if let Entry::Occupied(mut entry) = self.wakeups.entry(tick) {
entry.get_mut().retain(|wakeup| !pruned.contains(wakeup));
if entry.get().is_empty() {
let _ = entry.remove();
}
}
}
}
// Get the wakeup for a particular block/candidate combo, if any.
fn wakeup_for(&self, block_hash: Hash, candidate_hash: CandidateHash) -> Option<Tick> {
self.reverse_wakeups.get(&(block_hash, candidate_hash)).map(|t| *t)
@@ -379,30 +435,40 @@ trait DBReader {
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>>;
fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>>;
}
// This is a submodule to enforce opacity of the inner DB type.
mod approval_db_v1_reader {
use super::{
DBReader, KeyValueDB, Hash, CandidateHash, BlockEntry, CandidateEntry,
Arc, SubsystemResult, SubsystemError, approval_db,
SubsystemResult, SubsystemError, DatabaseConfig, approval_db,
};
/// A DB reader that uses the approval-db V1 under the hood.
pub(super) struct ApprovalDBV1Reader<T: ?Sized>(Arc<T>);
pub(super) struct ApprovalDBV1Reader<T> {
inner: T,
config: DatabaseConfig,
}
impl<T: ?Sized> From<Arc<T>> for ApprovalDBV1Reader<T> {
fn from(a: Arc<T>) -> Self {
ApprovalDBV1Reader(a)
impl<T> ApprovalDBV1Reader<T> {
pub(super) fn new(inner: T, config: DatabaseConfig) -> Self {
ApprovalDBV1Reader {
inner,
config,
}
}
}
impl DBReader for ApprovalDBV1Reader<dyn KeyValueDB> {
impl<'a, T: 'a> DBReader for ApprovalDBV1Reader<T>
where T: std::ops::Deref<Target=(dyn KeyValueDB + 'a)>
{
fn load_block_entry(
&self,
block_hash: &Hash,
) -> SubsystemResult<Option<BlockEntry>> {
approval_db::v1::load_block_entry(&*self.0, block_hash)
approval_db::v1::load_block_entry(&*self.inner, &self.config, block_hash)
.map(|e| e.map(Into::into))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
@@ -411,14 +477,25 @@ mod approval_db_v1_reader {
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>> {
approval_db::v1::load_candidate_entry(&*self.0, candidate_hash)
approval_db::v1::load_candidate_entry(&*self.inner, &self.config, candidate_hash)
.map(|e| e.map(Into::into))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>> {
approval_db::v1::load_all_blocks(&*self.inner, &self.config)
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
}
}
use approval_db_v1_reader::ApprovalDBV1Reader;
struct ApprovalStatus {
required_tranches: RequiredTranches,
tranche_now: DelayTranche,
block_tick: Tick,
}
struct State<T> {
session_window: import::RollingSessionWindow,
keystore: Arc<LocalKeystore>,
@@ -432,12 +509,59 @@ impl<T> State<T> {
fn session_info(&self, i: SessionIndex) -> Option<&SessionInfo> {
self.session_window.session_info(i)
}
// Compute the required tranches for approval for this block and candidate combo.
// Fails if there is no approval entry for the block under the candidate or no candidate entry
// under the block, or if the session is out of bounds.
fn approval_status<'a, 'b>(
&'a self,
block_entry: &'a BlockEntry,
candidate_entry: &'b CandidateEntry,
) -> Option<(&'b ApprovalEntry, ApprovalStatus)> {
let session_info = match self.session_info(block_entry.session()) {
Some(s) => s,
None => {
tracing::warn!(target: LOG_TARGET, "Unknown session info for {}", block_entry.session());
return None;
}
};
let block_hash = block_entry.block_hash();
let tranche_now = self.clock.tranche_now(self.slot_duration_millis, block_entry.slot());
let block_tick = slot_number_to_tick(self.slot_duration_millis, block_entry.slot());
let no_show_duration = slot_number_to_tick(
self.slot_duration_millis,
Slot::from(u64::from(session_info.no_show_slots)),
);
if let Some(approval_entry) = candidate_entry.approval_entry(&block_hash) {
let required_tranches = approval_checking::tranches_to_approve(
approval_entry,
candidate_entry.approvals(),
tranche_now,
block_tick,
no_show_duration,
session_info.needed_approvals as _
);
let status = ApprovalStatus {
required_tranches,
block_tick,
tranche_now,
};
Some((approval_entry, status))
} else {
None
}
}
}
#[derive(Debug)]
enum Action {
ScheduleWakeup {
block_hash: Hash,
block_number: BlockNumber,
candidate_hash: CandidateHash,
tick: Tick,
},
@@ -451,6 +575,7 @@ enum Action {
candidate: CandidateReceipt,
backing_group: GroupIndex,
},
BecomeActive,
Conclude,
}
@@ -458,7 +583,7 @@ type BackgroundTaskMap = BTreeMap<BlockNumber, Vec<RemoteHandle<()>>>;
async fn run<C>(
mut ctx: C,
subsystem: ApprovalVotingSubsystem,
mut subsystem: ApprovalVotingSubsystem,
clock: Box<dyn Clock + Send + Sync>,
assignment_criteria: Box<dyn AssignmentCriteria + Send + Sync>,
) -> SubsystemResult<()>
@@ -469,7 +594,7 @@ async fn run<C>(
session_window: Default::default(),
keystore: subsystem.keystore,
slot_duration_millis: subsystem.slot_duration_millis,
db: ApprovalDBV1Reader::from(subsystem.db.clone()),
db: ApprovalDBV1Reader::new(subsystem.db.clone(), subsystem.db_config.clone()),
clock,
assignment_criteria,
};
@@ -496,20 +621,28 @@ async fn run<C>(
)?
}
next_msg = ctx.recv().fuse() => {
let actions = handle_from_overseer(
let mut actions = handle_from_overseer(
&mut ctx,
&mut state,
&subsystem.metrics,
db_writer,
subsystem.db_config,
next_msg?,
&mut last_finalized_height,
&wakeups,
&mut wakeups,
).await?;
if let Some(finalized_height) = last_finalized_height {
cleanup_background_tasks(finalized_height, &mut background_tasks);
}
if let Mode::Syncing(ref mut oracle) = subsystem.mode {
if !oracle.is_major_syncing() {
// note that we're active before processing other actions.
actions.insert(0, Action::BecomeActive)
}
}
actions
}
background_request = background_rx.next().fuse() => {
@@ -531,8 +664,10 @@ async fn run<C>(
&subsystem.metrics,
&mut wakeups,
db_writer,
subsystem.db_config,
&background_tx,
&mut background_tasks,
&mut subsystem.mode,
actions,
).await? {
break;
@@ -548,20 +683,25 @@ async fn handle_actions(
metrics: &Metrics,
wakeups: &mut Wakeups,
db: &dyn KeyValueDB,
db_config: DatabaseConfig,
background_tx: &mpsc::Sender<BackgroundRequest>,
background_tasks: &mut BackgroundTaskMap,
mode: &mut Mode,
actions: impl IntoIterator<Item = Action>,
) -> SubsystemResult<bool> {
let mut transaction = approval_db::v1::Transaction::default();
let mut transaction = approval_db::v1::Transaction::new(db_config);
let mut conclude = false;
for action in actions {
match action {
Action::ScheduleWakeup {
block_hash,
block_number,
candidate_hash,
tick,
} => wakeups.schedule(block_hash, candidate_hash, tick),
} => {
wakeups.schedule(block_hash, block_number, candidate_hash, tick)
}
Action::WriteBlockEntry(block_entry) => {
transaction.put_block_entry(block_entry.into());
}
@@ -576,6 +716,9 @@ async fn handle_actions(
candidate,
backing_group,
} => {
// Don't launch approval work if the node is syncing.
if let Mode::Syncing(_) = *mode { continue }
metrics.on_assignment_produced();
let block_hash = indirect_cert.block_hash;
let validator_index = indirect_cert.validator;
@@ -600,6 +743,15 @@ async fn handle_actions(
background_tasks.entry(relay_block_number).or_default().push(handle);
}
}
Action::BecomeActive => {
*mode = Mode::Active;
let messages = distribution_messages_for_activation(
ApprovalDBV1Reader::new(db, db_config)
)?;
ctx.send_messages(messages.into_iter().map(Into::into)).await;
}
Action::Conclude => { conclude = true; }
}
}
@@ -627,15 +779,113 @@ fn cleanup_background_tasks(
// the task on drop.
}
fn distribution_messages_for_activation<'a>(
db: impl DBReader + 'a,
) -> SubsystemResult<Vec<ApprovalDistributionMessage>> {
let all_blocks = db.load_all_blocks()?;
let mut approval_meta = Vec::with_capacity(all_blocks.len());
let mut messages = Vec::new();
messages.push(ApprovalDistributionMessage::NewBlocks(Vec::new())); // dummy value.
for block_hash in all_blocks {
let block_entry = match db.load_block_entry(&block_hash)? {
Some(b) => b,
None => {
tracing::warn!(
target: LOG_TARGET,
?block_hash,
"Missing block entry",
);
continue
}
};
approval_meta.push(BlockApprovalMeta {
hash: block_hash,
number: block_entry.block_number(),
parent_hash: block_entry.parent_hash(),
candidates: block_entry.candidates().iter().map(|(_, c_hash)| *c_hash).collect(),
slot: block_entry.slot(),
});
for (i, (_, candidate_hash)) in block_entry.candidates().iter().enumerate() {
let candidate_entry = match db.load_candidate_entry(&candidate_hash)? {
Some(c) => c,
None => {
tracing::warn!(
target: LOG_TARGET,
?block_hash,
?candidate_hash,
"Missing candidate entry",
);
continue
}
};
match candidate_entry.approval_entry(&block_hash) {
Some(approval_entry) => {
match approval_entry.local_statements() {
(None, None) | (None, Some(_)) => {}, // second is impossible case.
(Some(assignment), None) => {
messages.push(ApprovalDistributionMessage::DistributeAssignment(
IndirectAssignmentCert {
block_hash,
validator: assignment.validator_index(),
cert: assignment.cert().clone(),
},
i as _,
));
}
(Some(assignment), Some(approval_sig)) => {
messages.push(ApprovalDistributionMessage::DistributeAssignment(
IndirectAssignmentCert {
block_hash,
validator: assignment.validator_index(),
cert: assignment.cert().clone(),
},
i as _,
));
messages.push(ApprovalDistributionMessage::DistributeApproval(
IndirectSignedApprovalVote {
block_hash,
candidate_index: i as _,
validator: assignment.validator_index(),
signature: approval_sig,
}
))
}
}
}
None => {
tracing::warn!(
target: LOG_TARGET,
?block_hash,
?candidate_hash,
"Missing approval entry",
);
}
}
}
}
messages[0] = ApprovalDistributionMessage::NewBlocks(approval_meta);
Ok(messages)
}
// Handle an incoming signal from the overseer. Returns true if execution should conclude.
async fn handle_from_overseer(
ctx: &mut impl SubsystemContext,
state: &mut State<impl DBReader>,
metrics: &Metrics,
db_writer: &dyn KeyValueDB,
db_config: DatabaseConfig,
x: FromOverseer<ApprovalVotingMessage>,
last_finalized_height: &mut Option<BlockNumber>,
wakeups: &Wakeups,
wakeups: &mut Wakeups,
) -> SubsystemResult<Vec<Action>> {
let actions = match x {
@@ -648,6 +898,7 @@ async fn handle_from_overseer(
ctx,
state,
db_writer,
db_config,
head,
&*last_finalized_height,
).await {
@@ -685,6 +936,7 @@ async fn handle_from_overseer(
// and approvals which trigger rescheduling.
actions.push(Action::ScheduleWakeup {
block_hash: block_batch.block_hash,
block_number: block_batch.block_number,
candidate_hash: c_hash,
tick,
});
@@ -700,9 +952,11 @@ async fn handle_from_overseer(
FromOverseer::Signal(OverseerSignal::BlockFinalized(block_hash, block_number)) => {
*last_finalized_height = Some(block_number);
approval_db::v1::canonicalize(db_writer, block_number, block_hash)
approval_db::v1::canonicalize(db_writer, &db_config, block_number, block_hash)
.map_err(|e| SubsystemError::with_origin("db", e))?;
wakeups.prune_finalized_wakeups(block_number);
Vec::new()
}
FromOverseer::Signal(OverseerSignal::Conclude) => {
@@ -711,7 +965,7 @@ async fn handle_from_overseer(
FromOverseer::Communication { msg } => match msg {
ApprovalVotingMessage::CheckAndImportAssignment(a, claimed_core, res) => {
let (check_outcome, actions)
= check_and_import_assignment(state, metrics, a, claimed_core)?;
= check_and_import_assignment(state, a, claimed_core)?;
let _ = res.send(check_outcome);
actions
}
@@ -996,6 +1250,7 @@ fn min_prefer_some<T: std::cmp::Ord>(
fn schedule_wakeup_action(
approval_entry: &ApprovalEntry,
block_hash: Hash,
block_number: BlockNumber,
candidate_hash: CandidateHash,
block_tick: Tick,
required_tranches: RequiredTranches,
@@ -1005,6 +1260,7 @@ fn schedule_wakeup_action(
RequiredTranches::All => None,
RequiredTranches::Exact { next_no_show, .. } => next_no_show.map(|tick| Action::ScheduleWakeup {
block_hash,
block_number,
candidate_hash,
tick,
}),
@@ -1032,7 +1288,12 @@ fn schedule_wakeup_action(
};
min_prefer_some(next_non_empty_tranche, next_no_show)
.map(|tick| Action::ScheduleWakeup { block_hash, candidate_hash, tick })
.map(|tick| Action::ScheduleWakeup {
block_hash,
block_number,
candidate_hash,
tick,
})
}
};
@@ -1060,7 +1321,6 @@ fn schedule_wakeup_action(
fn check_and_import_assignment(
state: &State<impl DBReader>,
metrics: &Metrics,
assignment: IndirectAssignmentCert,
candidate_index: CandidateIndex,
) -> SubsystemResult<(AssignmentCheckResult, Vec<Action>)> {
@@ -1155,24 +1415,22 @@ fn check_and_import_assignment(
}
};
// We check for approvals here because we may be late in seeing a block containing a
// candidate for which we have already seen approvals by the same validator.
//
// For these candidates, we will receive the assignments potentially after a corresponding
// approval, and so we must check for approval here.
//
// Note that this already produces actions for writing
// the candidate entry and any modified block entries to disk.
//
// It also produces actions to schedule wakeups for the candidate.
let actions = check_and_apply_full_approval(
state,
&metrics,
Some((assignment.block_hash, block_entry)),
assigned_candidate_hash,
candidate_entry,
|h, _| h == &assignment.block_hash,
)?;
let mut actions = Vec::new();
// We've imported a new approval, so we need to schedule a wake-up for when that might no-show.
if let Some((approval_entry, status)) = state.approval_status(&block_entry, &candidate_entry) {
actions.extend(schedule_wakeup_action(
approval_entry,
block_entry.block_hash(),
block_entry.block_number(),
assigned_candidate_hash,
status.block_tick,
status.required_tranches,
));
}
// We also write the candidate entry as it now contains the new candidate.
actions.push(Action::WriteCandidateEntry(assigned_candidate_hash, candidate_entry));
Ok((res, actions))
}
@@ -1259,116 +1517,89 @@ fn check_and_import_approval<T>(
let actions = import_checked_approval(
state,
&metrics,
Some((approval.block_hash, block_entry)),
block_entry,
approved_candidate_hash,
candidate_entry,
approval.validator,
)?;
ApprovalSource::Remote(approval.validator),
);
Ok((actions, t))
}
enum ApprovalSource {
Remote(ValidatorIndex),
Local(ValidatorIndex, ValidatorSignature),
}
impl ApprovalSource {
fn validator_index(&self) -> ValidatorIndex {
match *self {
ApprovalSource::Remote(v) | ApprovalSource::Local(v, _) => v,
}
}
fn is_remote(&self) -> bool {
match *self {
ApprovalSource::Remote(_) => true,
ApprovalSource::Local(_, _) => false,
}
}
}
// Import an approval vote which is already checked to be valid and corresponding to an assigned
// validator on the candidate and block. This updates the block entry and candidate entry as
// necessary and schedules any further wakeups.
fn import_checked_approval(
state: &State<impl DBReader>,
metrics: &Metrics,
already_loaded: Option<(Hash, BlockEntry)>,
mut block_entry: BlockEntry,
candidate_hash: CandidateHash,
mut candidate_entry: CandidateEntry,
validator: ValidatorIndex,
) -> SubsystemResult<Vec<Action>> {
if candidate_entry.mark_approval(validator) {
// already approved - nothing to do here.
return Ok(Vec::new());
source: ApprovalSource,
) -> Vec<Action> {
let validator_index = source.validator_index();
let already_approved_by = candidate_entry.mark_approval(validator_index);
let candidate_approved_in_block = block_entry.is_candidate_approved(&candidate_hash);
// Check for early exits.
//
// If the candidate was approved
// but not the block, it means that we still need more approvals for the candidate under the
// block.
//
// If the block was approved, but the validator hadn't approved it yet, we should still hold
// onto the approval vote on-disk in case we restart and rebroadcast votes. Otherwise, our
// assignment might manifest as a no-show.
match source {
ApprovalSource::Remote(_) => {
// We don't store remote votes, so we can early exit as long at the candidate is
// already concluded under the block i.e. we don't need more approvals.
if candidate_approved_in_block {
return Vec::new();
}
}
ApprovalSource::Local(_, _) => {
// We never early return on the local validator.
}
}
// Check if this approval vote alters the approval state of any blocks.
//
// This may include blocks beyond the already loaded block.
let actions = check_and_apply_full_approval(
state,
metrics,
already_loaded,
candidate_hash,
candidate_entry,
|_, a| a.is_assigned(validator),
)?;
Ok(actions)
}
// Checks the candidate for full approval under all blocks matching the given filter.
//
// If returning without error, is guaranteed to have produced actions
// to write all modified block entries. It also schedules wakeups for
// the candidate under any blocks filtered.
fn check_and_apply_full_approval(
state: &State<impl DBReader>,
metrics: &Metrics,
mut already_loaded: Option<(Hash, BlockEntry)>,
candidate_hash: CandidateHash,
mut candidate_entry: CandidateEntry,
filter: impl Fn(&Hash, &ApprovalEntry) -> bool,
) -> SubsystemResult<Vec<Action>> {
// We only query this max once per hash.
let db = &state.db;
let mut load_block_entry = move |block_hash| -> SubsystemResult<Option<BlockEntry>> {
if already_loaded.as_ref().map_or(false, |(h, _)| h == block_hash) {
Ok(already_loaded.take().map(|(_, c)| c))
} else {
db.load_block_entry(block_hash)
}
};
let mut newly_approved = Vec::new();
let mut actions = Vec::new();
for (block_hash, approval_entry) in candidate_entry.iter_approval_entries()
.into_iter()
.filter(|(h, a)| !a.is_approved() && filter(h, a))
let block_hash = block_entry.block_hash();
let block_number = block_entry.block_number();
let (is_approved, status) = if let Some((approval_entry, status))
= state.approval_status(&block_entry, &candidate_entry)
{
let mut block_entry = match load_block_entry(block_hash)? {
None => {
tracing::warn!(
target: LOG_TARGET,
"Missing block entry {} referenced by candidate {}",
block_hash,
candidate_hash,
);
continue
}
Some(b) => b,
};
let session_info = match state.session_info(block_entry.session()) {
Some(s) => s,
None => {
tracing::warn!(target: LOG_TARGET, "Unknown session info for {}", block_entry.session());
continue
}
};
let tranche_now = state.clock.tranche_now(state.slot_duration_millis, block_entry.slot());
let block_tick = slot_number_to_tick(state.slot_duration_millis, block_entry.slot());
let no_show_duration = slot_number_to_tick(
state.slot_duration_millis,
Slot::from(u64::from(session_info.no_show_slots)),
);
let required_tranches = approval_checking::tranches_to_approve(
approval_entry,
candidate_entry.approvals(),
tranche_now,
block_tick,
no_show_duration,
session_info.needed_approvals as _
);
let check = approval_checking::check_approval(
&candidate_entry,
approval_entry,
required_tranches.clone(),
status.required_tranches.clone(),
);
if check.is_approved() {
let is_approved = check.is_approved();
if is_approved {
tracing::trace!(
target: LOG_TARGET,
?candidate_hash,
@@ -1378,42 +1609,74 @@ fn check_and_apply_full_approval(
let no_shows = check.known_no_shows();
let was_approved = block_entry.is_fully_approved();
newly_approved.push(*block_hash);
let was_block_approved = block_entry.is_fully_approved();
block_entry.mark_approved_by_hash(&candidate_hash);
let is_approved = block_entry.is_fully_approved();
let is_block_approved = block_entry.is_fully_approved();
if no_shows != 0 {
metrics.on_no_shows(no_shows);
}
metrics.on_candidate_approved(tranche_now as _);
metrics.on_candidate_approved(status.tranche_now as _);
if is_approved && !was_approved {
metrics.on_block_approved(tranche_now as _)
if is_block_approved && !was_block_approved {
metrics.on_block_approved(status.tranche_now as _);
}
actions.push(Action::WriteBlockEntry(block_entry));
}
(is_approved, status)
} else {
tracing::warn!(
target: LOG_TARGET,
?candidate_hash,
?block_hash,
?validator_index,
"No approval entry for approval under block",
);
return Vec::new();
};
{
let approval_entry = candidate_entry.approval_entry_mut(&block_hash)
.expect("Approval entry just fetched; qed");
let was_approved = approval_entry.is_approved();
let newly_approved = is_approved && !was_approved;
if is_approved {
approval_entry.mark_approved();
}
if let ApprovalSource::Local(_, ref sig) = source {
approval_entry.import_approval_sig(sig.clone());
}
actions.extend(schedule_wakeup_action(
&approval_entry,
*block_hash,
block_hash,
block_number,
candidate_hash,
block_tick,
required_tranches,
status.block_tick,
status.required_tranches,
));
}
for b in &newly_approved {
if let Some(a) = candidate_entry.approval_entry_mut(b) {
a.mark_approved();
// We have no need to write the candidate entry if
//
// 1. The source is remote, as we don't store anything new in the approval entry.
// 2. The candidate is not newly approved, as we haven't altered the approval entry's
// approved flag with `mark_approved` above.
// 3. The source had already approved the candidate, as we haven't altered the bitfield.
if !source.is_remote() || newly_approved || !already_approved_by {
// In all other cases, we need to write the candidate entry.
actions.push(Action::WriteCandidateEntry(candidate_hash, candidate_entry));
}
}
actions.push(Action::WriteCandidateEntry(candidate_hash, candidate_entry));
Ok(actions)
actions
}
fn should_trigger_assignment(
@@ -1592,6 +1855,7 @@ fn process_wakeup(
actions.extend(schedule_wakeup_action(
&approval_entry,
relay_block,
block_entry.block_number(),
candidate_hash,
block_tick,
tranches_to_approve,
@@ -1863,11 +2127,11 @@ fn issue_approval(
let actions = import_checked_approval(
state,
metrics,
Some((block_hash, block_entry)),
block_entry,
candidate_hash,
candidate_entry,
validator_index as _,
)?;
ApprovalSource::Local(validator_index as _, sig.clone()),
);
metrics.on_approval_produced();
@@ -23,7 +23,7 @@
use polkadot_node_primitives::approval::{DelayTranche, RelayVRFStory, AssignmentCert};
use polkadot_primitives::v1::{
ValidatorIndex, CandidateReceipt, SessionIndex, GroupIndex, CoreIndex,
Hash, CandidateHash, BlockNumber,
Hash, CandidateHash, BlockNumber, ValidatorSignature,
};
use sp_consensus_slots::Slot;
@@ -79,6 +79,7 @@ pub struct ApprovalEntry {
tranches: Vec<TrancheEntry>,
backing_group: GroupIndex,
our_assignment: Option<OurAssignment>,
our_approval_sig: Option<ValidatorSignature>,
// `n_validators` bits.
assignments: BitVec<BitOrderLsb0, u8>,
approved: bool,
@@ -108,6 +109,11 @@ impl ApprovalEntry {
})
}
/// Import our local approval vote signature for this candidate.
pub fn import_approval_sig(&mut self, approval_sig: ValidatorSignature) {
self.our_approval_sig = Some(approval_sig);
}
/// Whether a validator is already assigned.
pub fn is_assigned(&self, validator_index: ValidatorIndex) -> bool {
self.assignments.get(validator_index.0 as usize).map(|b| *b).unwrap_or(false)
@@ -190,6 +196,18 @@ impl ApprovalEntry {
self.backing_group
}
/// 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>) {
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)
} else {
(None, None)
}
}
/// For tests: set our assignment.
#[cfg(test)]
pub fn set_our_assignment(&mut self, our_assignment: OurAssignment) {
@@ -203,6 +221,7 @@ impl From<crate::approval_db::v1::ApprovalEntry> for ApprovalEntry {
tranches: entry.tranches.into_iter().map(Into::into).collect(),
backing_group: entry.backing_group,
our_assignment: entry.our_assignment.map(Into::into),
our_approval_sig: entry.our_approval_sig.map(Into::into),
assignments: entry.assignments,
approved: entry.approved,
}
@@ -215,6 +234,7 @@ impl From<ApprovalEntry> for crate::approval_db::v1::ApprovalEntry {
tranches: entry.tranches.into_iter().map(Into::into).collect(),
backing_group: entry.backing_group,
our_assignment: entry.our_assignment.map(Into::into),
our_approval_sig: entry.our_approval_sig.map(Into::into),
assignments: entry.assignments,
approved: entry.approved,
}
@@ -260,11 +280,6 @@ impl CandidateEntry {
self.block_assignments.get(block_hash)
}
/// Iterate over approval entries.
pub fn iter_approval_entries(&self) -> impl IntoIterator<Item = (&Hash, &ApprovalEntry)> {
self.block_assignments.iter()
}
#[cfg(test)]
pub fn add_approval_entry(
&mut self,
@@ -325,6 +340,13 @@ impl BlockEntry {
}
}
/// Whether a candidate is approved in the bitfield.
pub fn is_candidate_approved(&self, candidate_hash: &CandidateHash) -> bool {
self.candidates.iter().position(|(_, h)| h == candidate_hash)
.and_then(|p| self.approved_bitfield.get(p).map(|b| *b))
.unwrap_or(false)
}
/// Whether the block entry is fully approved.
pub fn is_fully_approved(&self) -> bool {
self.approved_bitfield.all()
@@ -339,18 +361,6 @@ impl BlockEntry {
})
}
#[cfg(test)]
pub fn block_hash(&self) -> Hash {
self.block_hash
}
#[cfg(test)]
pub fn is_candidate_approved(&self, candidate_hash: &CandidateHash) -> bool {
self.candidates.iter().position(|(_, h)| h == candidate_hash)
.and_then(|p| self.approved_bitfield.get(p).map(|b| *b))
.unwrap_or(false)
}
/// For tests: Add a candidate to the block entry. Returns the
/// index where the candidate was added.
///
@@ -402,6 +412,16 @@ impl BlockEntry {
pub fn block_number(&self) -> BlockNumber {
self.block_number
}
/// Access the block hash of the block entry.
pub fn block_hash(&self) -> Hash {
self.block_hash
}
/// Access the parent hash of the block entry.
pub fn parent_hash(&self) -> Hash {
self.parent_hash
}
}
impl From<crate::approval_db::v1::BlockEntry> for BlockEntry {
+294 -227
View File
@@ -179,6 +179,14 @@ impl DBReader for TestStore {
) -> SubsystemResult<Option<CandidateEntry>> {
Ok(self.candidate_entries.get(candidate_hash).cloned())
}
fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>> {
let mut hashes: Vec<_> = self.block_entries.keys().cloned().collect();
hashes.sort_by_key(|k| self.block_entries.get(k).unwrap().block_number());
Ok(hashes)
}
}
fn blank_state() -> State<TestStore> {
@@ -226,6 +234,7 @@ fn sign_approval(
key.sign(&super::approval_signing_payload(ApprovalVote(candidate_hash), session_index)).into()
}
#[derive(Clone)]
struct StateConfig {
session_index: SessionIndex,
slot: Slot,
@@ -353,6 +362,7 @@ fn add_candidate_to_block(
tranches: Vec::new(),
backing_group,
our_assignment: None,
our_approval_sig: None,
assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; n_validators],
approved: false,
}.into(),
@@ -378,7 +388,6 @@ fn rejects_bad_assignment() {
let res = check_and_import_assignment(
&mut state,
&Metrics(None),
assignment_good.clone(),
candidate_index,
).unwrap();
@@ -399,7 +408,6 @@ fn rejects_bad_assignment() {
let res = check_and_import_assignment(
&mut state,
&Metrics(None),
assignment,
candidate_index,
).unwrap();
@@ -415,7 +423,6 @@ fn rejects_bad_assignment() {
// same assignment, but this time rejected
let res = check_and_import_assignment(
&mut state,
&Metrics(None),
assignment_good,
candidate_index,
).unwrap();
@@ -446,7 +453,6 @@ fn rejects_assignment_in_future() {
let res = check_and_import_assignment(
&mut state,
&Metrics(None),
assignment.clone(),
candidate_index,
).unwrap();
@@ -461,7 +467,6 @@ fn rejects_assignment_in_future() {
let res = check_and_import_assignment(
&mut state,
&Metrics(None),
assignment.clone(),
candidate_index,
).unwrap();
@@ -486,7 +491,6 @@ fn rejects_assignment_with_unknown_candidate() {
let res = check_and_import_assignment(
&mut state,
&Metrics(None),
assignment.clone(),
candidate_index,
).unwrap();
@@ -518,7 +522,6 @@ fn assignment_import_updates_candidate_entry_and_schedules_wakeup() {
let (res, actions) = check_and_import_assignment(
&mut state,
&Metrics(None),
assignment.clone(),
candidate_index,
).unwrap();
@@ -532,6 +535,7 @@ fn assignment_import_updates_candidate_entry_and_schedules_wakeup() {
block_hash: b,
candidate_hash: c,
tick,
..
} => {
assert_eq!(b, &block_hash);
assert_eq!(c, &candidate_hash);
@@ -700,10 +704,11 @@ fn accepts_and_imports_approval_after_assignment() {
}
#[test]
fn second_approval_import_is_no_op() {
fn second_approval_import_only_schedules_wakeups() {
let block_hash = Hash::repeat_byte(0x01);
let candidate_hash = CandidateHash(Hash::repeat_byte(0xCC));
let validator_index = ValidatorIndex(0);
let validator_index_b = ValidatorIndex(1);
let candidate_index = 0;
let mut state = State {
@@ -733,6 +738,25 @@ fn second_approval_import_is_no_op() {
assert!(!state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.mark_approval(validator_index));
// There is only one assignment, so nothing to schedule if we double-import.
let (actions, res) = check_and_import_approval(
&state,
&Metrics(None),
vote.clone(),
|r| r
).unwrap();
assert_eq!(res, ApprovalCheckResult::Accepted);
assert!(actions.is_empty());
// After adding a second assignment, there should be a schedule wakeup action.
state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.approval_entry_mut(&block_hash)
.unwrap()
.import_assignment(0, validator_index_b, 0);
let (actions, res) = check_and_import_approval(
&state,
&Metrics(None),
@@ -741,11 +765,16 @@ fn second_approval_import_is_no_op() {
).unwrap();
assert_eq!(res, ApprovalCheckResult::Accepted);
assert!(actions.is_empty())
assert_eq!(actions.len(), 1);
assert_matches!(
actions.get(0).unwrap(),
Action::ScheduleWakeup { .. } => {}
);
}
#[test]
fn check_and_apply_full_approval_sets_flag_and_bit() {
fn import_checked_approval_updates_entries_and_schedules() {
let block_hash = Hash::repeat_byte(0x01);
let candidate_hash = CandidateHash(Hash::repeat_byte(0xCC));
let validator_index_a = ValidatorIndex(0);
@@ -773,101 +802,67 @@ fn check_and_apply_full_approval_sets_flag_and_bit() {
.unwrap()
.import_assignment(0, validator_index_b, 0);
assert!(!state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.mark_approval(validator_index_a));
{
let mut actions = import_checked_approval(
&state,
&Metrics(None),
state.db.block_entries.get(&block_hash).unwrap().clone(),
candidate_hash,
state.db.candidate_entries.get(&candidate_hash).unwrap().clone(),
ApprovalSource::Remote(validator_index_a),
);
assert!(!state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.mark_approval(validator_index_b));
assert_eq!(actions.len(), 2);
assert_matches!(
actions.get(0).unwrap(),
Action::ScheduleWakeup {
block_hash: b_hash,
candidate_hash: c_hash,
..
} => {
assert_eq!(b_hash, &block_hash);
assert_eq!(c_hash, &candidate_hash);
}
);
assert_matches!(
actions.get_mut(1).unwrap(),
Action::WriteCandidateEntry(c_hash, ref mut c_entry) => {
assert_eq!(c_hash, &candidate_hash);
assert!(!c_entry.approval_entry(&block_hash).unwrap().is_approved());
assert!(c_entry.mark_approval(validator_index_a));
let actions = check_and_apply_full_approval(
&state,
&Metrics(None),
None,
candidate_hash,
state.db.candidate_entries.get(&candidate_hash).unwrap().clone(),
|b_hash, _a| b_hash == &block_hash,
).unwrap();
state.db.candidate_entries.insert(candidate_hash, c_entry.clone());
}
);
}
assert_eq!(actions.len(), 2);
assert_matches!(
actions.get(0).unwrap(),
Action::WriteBlockEntry(b_entry) => {
assert_eq!(b_entry.block_hash(), block_hash);
assert!(b_entry.is_fully_approved());
assert!(b_entry.is_candidate_approved(&candidate_hash));
}
);
assert_matches!(
actions.get(1).unwrap(),
Action::WriteCandidateEntry(c_hash, c_entry) => {
assert_eq!(c_hash, &candidate_hash);
assert!(c_entry.approval_entry(&block_hash).unwrap().is_approved());
}
);
}
{
let mut actions = import_checked_approval(
&state,
&Metrics(None),
state.db.block_entries.get(&block_hash).unwrap().clone(),
candidate_hash,
state.db.candidate_entries.get(&candidate_hash).unwrap().clone(),
ApprovalSource::Remote(validator_index_b),
);
#[test]
fn check_and_apply_full_approval_does_not_load_cached_block_from_db() {
let block_hash = Hash::repeat_byte(0x01);
let candidate_hash = CandidateHash(Hash::repeat_byte(0xCC));
let validator_index_a = ValidatorIndex(0);
let validator_index_b = ValidatorIndex(1);
let mut state = State {
assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| {
Ok(0)
})),
..some_state(StateConfig {
validators: vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob, Sr25519Keyring::Charlie],
validator_groups: vec![vec![ValidatorIndex(0), ValidatorIndex(1)], vec![ValidatorIndex(2)]],
needed_approvals: 2,
..Default::default()
})
};
state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.approval_entry_mut(&block_hash)
.unwrap()
.import_assignment(0, validator_index_a, 0);
state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.approval_entry_mut(&block_hash)
.unwrap()
.import_assignment(0, validator_index_b, 0);
assert!(!state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.mark_approval(validator_index_a));
assert!(!state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.mark_approval(validator_index_b));
let block_entry = state.db.block_entries.remove(&block_hash).unwrap();
let actions = check_and_apply_full_approval(
&state,
&Metrics(None),
Some((block_hash, block_entry)),
candidate_hash,
state.db.candidate_entries.get(&candidate_hash).unwrap().clone(),
|b_hash, _a| b_hash == &block_hash,
).unwrap();
assert_eq!(actions.len(), 2);
assert_matches!(
actions.get(0).unwrap(),
Action::WriteBlockEntry(b_entry) => {
assert_eq!(b_entry.block_hash(), block_hash);
assert!(b_entry.is_fully_approved());
assert!(b_entry.is_candidate_approved(&candidate_hash));
}
);
assert_matches!(
actions.get(1).unwrap(),
Action::WriteCandidateEntry(c_hash, c_entry) => {
assert_eq!(c_hash, &candidate_hash);
assert!(c_entry.approval_entry(&block_hash).unwrap().is_approved());
}
);
assert_matches!(
actions.get(0).unwrap(),
Action::WriteBlockEntry(b_entry) => {
assert_eq!(b_entry.block_hash(), block_hash);
assert!(b_entry.is_fully_approved());
assert!(b_entry.is_candidate_approved(&candidate_hash));
}
);
assert_matches!(
actions.get_mut(1).unwrap(),
Action::WriteCandidateEntry(c_hash, ref mut c_entry) => {
assert_eq!(c_hash, &candidate_hash);
assert!(c_entry.approval_entry(&block_hash).unwrap().is_approved());
assert!(c_entry.mark_approval(validator_index_b));
}
);
}
}
#[test]
@@ -886,6 +881,7 @@ fn assignment_triggered_by_all_with_less_than_threshold() {
validator_index: ValidatorIndex(4),
triggered: false,
}),
our_approval_sig: None,
assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
approved: false,
};
@@ -931,6 +927,7 @@ fn assignment_not_triggered_by_all_with_threshold() {
validator_index: ValidatorIndex(4),
triggered: false,
}),
our_approval_sig: None,
assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
approved: false,
};
@@ -982,6 +979,7 @@ fn assignment_not_triggered_if_already_triggered() {
validator_index: ValidatorIndex(4),
triggered: true,
}),
our_approval_sig: None,
assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
approved: false,
};
@@ -1019,6 +1017,7 @@ fn assignment_not_triggered_by_exact() {
validator_index: ValidatorIndex(4),
triggered: false,
}),
our_approval_sig: None,
assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
approved: false,
};
@@ -1057,6 +1056,7 @@ fn assignment_not_triggered_more_than_maximum() {
validator_index: ValidatorIndex(4),
triggered: false,
}),
our_approval_sig: None,
assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
approved: false,
};
@@ -1100,6 +1100,7 @@ fn assignment_triggered_if_at_maximum() {
validator_index: ValidatorIndex(4),
triggered: false,
}),
our_approval_sig: None,
assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
approved: false,
};
@@ -1143,6 +1144,7 @@ fn assignment_not_triggered_if_at_maximum_but_clock_is_before() {
validator_index: ValidatorIndex(4),
triggered: false,
}),
our_approval_sig: None,
assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
approved: false,
};
@@ -1186,6 +1188,7 @@ fn assignment_not_triggered_if_at_maximum_but_clock_is_before_with_drift() {
validator_index: ValidatorIndex(4),
triggered: false,
}),
our_approval_sig: None,
assignments: bitvec::bitvec![BitOrderLsb0, u8; 0; 4],
approved: false,
};
@@ -1222,9 +1225,9 @@ fn wakeups_next() {
let c_a = CandidateHash(Hash::repeat_byte(2));
let c_b = CandidateHash(Hash::repeat_byte(3));
wakeups.schedule(b_a, c_a, 1);
wakeups.schedule(b_a, c_b, 4);
wakeups.schedule(b_b, c_b, 3);
wakeups.schedule(b_a, 0, c_a, 1);
wakeups.schedule(b_a, 0, c_b, 4);
wakeups.schedule(b_b, 1, c_b, 3);
assert_eq!(wakeups.first().unwrap(), 1);
@@ -1237,6 +1240,28 @@ fn wakeups_next() {
assert_eq!(wakeups.next(&clock).await, (4, b_a, c_b));
assert!(wakeups.first().is_none());
assert!(wakeups.wakeups.is_empty());
assert_eq!(
wakeups.block_numbers.get(&0).unwrap(),
&vec![b_a].into_iter().collect::<HashSet<_>>(),
);
assert_eq!(
wakeups.block_numbers.get(&1).unwrap(),
&vec![b_b].into_iter().collect::<HashSet<_>>(),
);
wakeups.prune_finalized_wakeups(0);
assert!(wakeups.block_numbers.get(&0).is_none());
assert_eq!(
wakeups.block_numbers.get(&1).unwrap(),
&vec![b_b].into_iter().collect::<HashSet<_>>(),
);
wakeups.prune_finalized_wakeups(1);
assert!(wakeups.block_numbers.get(&0).is_none());
assert!(wakeups.block_numbers.get(&1).is_none());
});
let aux_fut = Box::pin(async move {
@@ -1255,9 +1280,9 @@ fn wakeup_earlier_supersedes_later() {
let b_a = Hash::repeat_byte(0);
let c_a = CandidateHash(Hash::repeat_byte(2));
wakeups.schedule(b_a, c_a, 4);
wakeups.schedule(b_a, c_a, 2);
wakeups.schedule(b_a, c_a, 3);
wakeups.schedule(b_a, 0, c_a, 4);
wakeups.schedule(b_a, 0, c_a, 2);
wakeups.schedule(b_a, 0, c_a, 3);
let clock = MockClock::new(0);
let clock_aux = clock.clone();
@@ -1276,7 +1301,7 @@ fn wakeup_earlier_supersedes_later() {
}
#[test]
fn block_not_approved_until_all_candidates_approved() {
fn import_checked_approval_sets_one_block_bit_at_a_time() {
let block_hash = Hash::repeat_byte(0x01);
let candidate_hash = CandidateHash(Hash::repeat_byte(0xCC));
let candidate_hash_2 = CandidateHash(Hash::repeat_byte(0xDD));
@@ -1305,7 +1330,7 @@ fn block_not_approved_until_all_candidates_approved() {
GroupIndex(1),
);
let approve_candidate = |db: &mut TestStore, c_hash| {
let setup_candidate = |db: &mut TestStore, c_hash| {
db.candidate_entries.get_mut(&c_hash).unwrap()
.approval_entry_mut(&block_hash)
.unwrap()
@@ -1318,27 +1343,51 @@ fn block_not_approved_until_all_candidates_approved() {
assert!(!db.candidate_entries.get_mut(&c_hash).unwrap()
.mark_approval(validator_index_a));
assert!(!db.candidate_entries.get_mut(&c_hash).unwrap()
.mark_approval(validator_index_b));
};
approve_candidate(&mut state.db, candidate_hash_2);
setup_candidate(&mut state.db, candidate_hash);
setup_candidate(&mut state.db, candidate_hash_2);
{
let b = state.db.block_entries.get_mut(&block_hash).unwrap();
b.mark_approved_by_hash(&candidate_hash);
assert!(!b.is_fully_approved());
}
let actions = check_and_apply_full_approval(
let actions = import_checked_approval(
&state,
&Metrics(None),
None,
state.db.block_entries.get(&block_hash).unwrap().clone(),
candidate_hash,
state.db.candidate_entries.get(&candidate_hash).unwrap().clone(),
ApprovalSource::Remote(validator_index_b),
);
assert_eq!(actions.len(), 2);
assert_matches!(
actions.get(0).unwrap(),
Action::WriteBlockEntry(b_entry) => {
assert_eq!(b_entry.block_hash(), block_hash);
assert!(!b_entry.is_fully_approved());
assert!(b_entry.is_candidate_approved(&candidate_hash));
assert!(!b_entry.is_candidate_approved(&candidate_hash_2));
state.db.block_entries.insert(block_hash, b_entry.clone());
}
);
assert_matches!(
actions.get(1).unwrap(),
Action::WriteCandidateEntry(c_h, c_entry) => {
assert_eq!(c_h, &candidate_hash);
assert!(c_entry.approval_entry(&block_hash).unwrap().is_approved());
state.db.candidate_entries.insert(*c_h, c_entry.clone());
}
);
let actions = import_checked_approval(
&state,
&Metrics(None),
state.db.block_entries.get(&block_hash).unwrap().clone(),
candidate_hash_2,
state.db.candidate_entries.get(&candidate_hash_2).unwrap().clone(),
|b_hash, _a| b_hash == &block_hash,
).unwrap();
ApprovalSource::Remote(validator_index_b),
);
assert_eq!(actions.len(), 2);
assert_matches!(
@@ -1346,6 +1395,7 @@ fn block_not_approved_until_all_candidates_approved() {
Action::WriteBlockEntry(b_entry) => {
assert_eq!(b_entry.block_hash(), block_hash);
assert!(b_entry.is_fully_approved());
assert!(b_entry.is_candidate_approved(&candidate_hash));
assert!(b_entry.is_candidate_approved(&candidate_hash_2));
}
);
@@ -1359,109 +1409,6 @@ fn block_not_approved_until_all_candidates_approved() {
);
}
#[test]
fn candidate_approval_applied_to_all_blocks() {
let block_hash = Hash::repeat_byte(0x01);
let block_hash_2 = Hash::repeat_byte(0x02);
let candidate_hash = CandidateHash(Hash::repeat_byte(0xCC));
let validator_index_a = ValidatorIndex(0);
let validator_index_b = ValidatorIndex(1);
let slot = Slot::from(1);
let session_index = 1;
let mut state = State {
assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| {
Ok(0)
})),
..some_state(StateConfig {
validators: vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob, Sr25519Keyring::Charlie],
validator_groups: vec![vec![ValidatorIndex(0), ValidatorIndex(1)], vec![ValidatorIndex(2)]],
needed_approvals: 2,
session_index,
slot,
..Default::default()
})
};
add_block(
&mut state.db,
block_hash_2,
session_index,
slot,
);
add_candidate_to_block(
&mut state.db,
block_hash_2,
candidate_hash,
3,
CoreIndex(1),
GroupIndex(1),
);
state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.approval_entry_mut(&block_hash)
.unwrap()
.import_assignment(0, validator_index_a, 0);
state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.approval_entry_mut(&block_hash)
.unwrap()
.import_assignment(0, validator_index_b, 0);
state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.approval_entry_mut(&block_hash_2)
.unwrap()
.import_assignment(0, validator_index_a, 0);
state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.approval_entry_mut(&block_hash_2)
.unwrap()
.import_assignment(0, validator_index_b, 0);
assert!(!state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.mark_approval(validator_index_a));
assert!(!state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.mark_approval(validator_index_b));
let actions = check_and_apply_full_approval(
&state,
&Metrics(None),
None,
candidate_hash,
state.db.candidate_entries.get(&candidate_hash).unwrap().clone(),
|_b_hash, _a| true,
).unwrap();
assert_eq!(actions.len(), 3);
assert_matches!(
actions.get(0).unwrap(),
Action::WriteBlockEntry(b_entry) => {
assert_eq!(b_entry.block_hash(), block_hash);
assert!(b_entry.is_fully_approved());
assert!(b_entry.is_candidate_approved(&candidate_hash));
}
);
assert_matches!(
actions.get(1).unwrap(),
Action::WriteBlockEntry(b_entry) => {
assert_eq!(b_entry.block_hash(), block_hash_2);
assert!(b_entry.is_fully_approved());
assert!(b_entry.is_candidate_approved(&candidate_hash));
}
);
assert_matches!(
actions.get(2).unwrap(),
Action::WriteCandidateEntry(c_hash, c_entry) => {
assert_eq!(c_hash, &candidate_hash);
assert!(c_entry.approval_entry(&block_hash).unwrap().is_approved());
assert!(c_entry.approval_entry(&block_hash_2).unwrap().is_approved());
}
);
}
#[test]
fn approved_ancestor_all_approved() {
let block_hash_1 = Hash::repeat_byte(0x01);
@@ -1760,7 +1707,7 @@ fn process_wakeup_schedules_wakeup() {
assert_eq!(actions.len(), 1);
assert_matches!(
actions.get(0).unwrap(),
Action::ScheduleWakeup { block_hash: b, candidate_hash: c, tick } => {
Action::ScheduleWakeup { block_hash: b, candidate_hash: c, tick, .. } => {
assert_eq!(b, &block_hash);
assert_eq!(c, &candidate_hash);
assert_eq!(tick, &(slot_to_tick(slot) + 10));
@@ -1777,3 +1724,123 @@ fn triggered_assignment_leads_to_recovery_and_validation() {
fn finalization_event_prunes() {
}
#[test]
fn local_approval_import_always_updates_approval_entry() {
let block_hash = Hash::repeat_byte(0x01);
let block_hash_2 = Hash::repeat_byte(0x02);
let candidate_hash = CandidateHash(Hash::repeat_byte(0xCC));
let validator_index = ValidatorIndex(0);
let state_config = StateConfig {
validators: vec![Sr25519Keyring::Alice, Sr25519Keyring::Bob, Sr25519Keyring::Charlie],
validator_groups: vec![vec![ValidatorIndex(0), ValidatorIndex(1)], vec![ValidatorIndex(2)]],
needed_approvals: 2,
..Default::default()
};
let mut state = State {
assignment_criteria: Box::new(MockAssignmentCriteria::check_only(|| {
Ok(0)
})),
..some_state(state_config.clone())
};
add_block(
&mut state.db,
block_hash_2,
state_config.session_index,
state_config.slot,
);
add_candidate_to_block(
&mut state.db,
block_hash_2,
candidate_hash,
state_config.validators.len(),
1.into(),
GroupIndex(1),
);
let sig_a = sign_approval(Sr25519Keyring::Alice, candidate_hash, 1);
let sig_b = sign_approval(Sr25519Keyring::Alice, candidate_hash, 1);
{
let mut import_local_assignment = |block_hash: Hash| {
let approval_entry = state.db.candidate_entries.get_mut(&candidate_hash).unwrap()
.approval_entry_mut(&block_hash)
.unwrap();
approval_entry.set_our_assignment(approval_db::v1::OurAssignment {
cert: garbage_assignment_cert(
AssignmentCertKind::RelayVRFModulo { sample: 0 }
),
tranche: 0,
validator_index,
triggered: false,
}.into());
assert!(approval_entry.trigger_our_assignment(0).is_some());
assert!(approval_entry.local_statements().0.is_some());
};
import_local_assignment(block_hash);
import_local_assignment(block_hash_2);
}
{
let mut actions = import_checked_approval(
&state,
&Metrics(None),
state.db.block_entries.get(&block_hash).unwrap().clone(),
candidate_hash,
state.db.candidate_entries.get(&candidate_hash).unwrap().clone(),
ApprovalSource::Local(validator_index, sig_a.clone()),
);
assert_eq!(actions.len(), 1);
assert_matches!(
actions.get_mut(0).unwrap(),
Action::WriteCandidateEntry(c_hash, ref mut c_entry) => {
assert_eq!(c_hash, &candidate_hash);
assert_eq!(
c_entry.approval_entry(&block_hash).unwrap().local_statements().1,
Some(sig_a),
);
assert!(c_entry.mark_approval(validator_index));
state.db.candidate_entries.insert(candidate_hash, c_entry.clone());
}
);
}
{
let mut actions = import_checked_approval(
&state,
&Metrics(None),
state.db.block_entries.get(&block_hash_2).unwrap().clone(),
candidate_hash,
state.db.candidate_entries.get(&candidate_hash).unwrap().clone(),
ApprovalSource::Local(validator_index, sig_b.clone()),
);
assert_eq!(actions.len(), 1);
assert_matches!(
actions.get_mut(0).unwrap(),
Action::WriteCandidateEntry(c_hash, ref mut c_entry) => {
assert_eq!(c_hash, &candidate_hash);
assert_eq!(
c_entry.approval_entry(&block_hash_2).unwrap().local_statements().1,
Some(sig_b),
);
assert!(c_entry.mark_approval(validator_index));
state.db.candidate_entries.insert(candidate_hash, c_entry.clone());
}
);
}
}
// TODO [now]: handling `BecomeActive` action broadcasts everything.