mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-14 05:11:09 +00:00
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:
committed by
GitHub
parent
01badafba6
commit
57b56770e0
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user