mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-17 00:51:06 +00:00
Dispute Coordinator Subsystem (#3150)
* skeleton for dispute-coordinator * add coordinator and participation message types * begin dispute-coordinator DB * functions for loading * implement strongly-typed DB transaction * add some tests for DB transaction * core logic for pruning * guide: update candidate-votes key for coordinator * update candidate-votes key * use big-endian encoding for session, and implement upper bound generator * finish implementing pruning * add a test for note_current_session * define state of the subsystem itself * barebones subsystem definition * control flow * more control flow * implement session-updating logic * trace * control flow for message handling * Update node/core/dispute-coordinator/src/lib.rs Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com> * Update node/subsystem/src/messages.rs Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com> * some more control flow * guide: remove overlay * more control flow * implement some DB getters * make progress on importing statements * add SignedDisputeStatement struct * move ApprovalVote to shared primitives * add a signing-payload API to explicit dispute statements * add signing-payload to CompactStatement * add relay-parent hash to seconded/valid dispute variatns * correct import * type-safe wrapper around dispute statements * use checked dispute statement in message type * extract rolling session window cache to subsystem-util * extract session window tests * approval-voting: use rolling session info cache * reduce dispute window to match runtime in practice * add byzantine_threshold and supermajority_threshold utilities to primitives * integrate rolling session window * Add PartialOrd to CandidateHash * add Ord to CandidateHash * implement active dispute update * add dispute messages to AllMessages * add dispute stubs to overseer * inform dispute participation to participate * implement issue_local_statement * implement `determine_undisputed_chain` * fix warnings * test harness for dispute coordinator tests * add more helpers to test harness * add some more helpers * some tests for dispute coordinator * ignore wrong validator indices * test finality voting rule constraint * add more tests * add variants to network bridge * fix test compilation * remove most dispute coordinator functionality as of #3222 we can do most of the work within the approval voting subsystem * Revert "remove most dispute coordinator functionality" This reverts commit 9cd615e8eb6ca0b382cbaff525d813e753d6004e. * Use thiserror Co-authored-by: Bernhard Schuster <bernhard@ahoi.io> * Update node/core/dispute-coordinator/src/lib.rs Co-authored-by: Bernhard Schuster <bernhard@ahoi.io> * extract tests to separate module * address nit * adjust run_iteration API Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com> Co-authored-by: Bernhard Schuster <bernhard@ahoi.io>
This commit is contained in:
committed by
GitHub
parent
7f344df160
commit
5bc2b2779d
Generated
+25
@@ -6091,6 +6091,30 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polkadot-node-core-dispute-coordinator"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"assert_matches",
|
||||||
|
"bitvec",
|
||||||
|
"derive_more",
|
||||||
|
"futures 0.3.15",
|
||||||
|
"kvdb",
|
||||||
|
"kvdb-memorydb",
|
||||||
|
"parity-scale-codec",
|
||||||
|
"polkadot-node-primitives",
|
||||||
|
"polkadot-node-subsystem",
|
||||||
|
"polkadot-node-subsystem-test-helpers",
|
||||||
|
"polkadot-node-subsystem-util",
|
||||||
|
"polkadot-primitives",
|
||||||
|
"sc-keystore",
|
||||||
|
"sp-core",
|
||||||
|
"sp-keyring",
|
||||||
|
"sp-keystore",
|
||||||
|
"thiserror",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polkadot-node-core-parachains-inherent"
|
name = "polkadot-node-core-parachains-inherent"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -6221,6 +6245,7 @@ dependencies = [
|
|||||||
"sp-consensus-babe",
|
"sp-consensus-babe",
|
||||||
"sp-consensus-vrf",
|
"sp-consensus-vrf",
|
||||||
"sp-core",
|
"sp-core",
|
||||||
|
"sp-keystore",
|
||||||
"sp-maybe-compressed-blob",
|
"sp-maybe-compressed-blob",
|
||||||
"sp-runtime",
|
"sp-runtime",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ members = [
|
|||||||
"node/core/bitfield-signing",
|
"node/core/bitfield-signing",
|
||||||
"node/core/candidate-validation",
|
"node/core/candidate-validation",
|
||||||
"node/core/chain-api",
|
"node/core/chain-api",
|
||||||
|
"node/core/dispute-coordinator",
|
||||||
"node/core/parachains-inherent",
|
"node/core/parachains-inherent",
|
||||||
"node/core/provisioner",
|
"node/core/provisioner",
|
||||||
"node/core/pvf",
|
"node/core/pvf",
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ pub type Hash = sp_core::H256;
|
|||||||
/// This type is produced by [`CandidateReceipt::hash`].
|
/// This type is produced by [`CandidateReceipt::hash`].
|
||||||
///
|
///
|
||||||
/// This type makes it easy to enforce that a hash is a candidate hash on the type level.
|
/// This type makes it easy to enforce that a hash is a candidate hash on the type level.
|
||||||
#[derive(Clone, Copy, Encode, Decode, Hash, Eq, PartialEq, Default)]
|
#[derive(Clone, Copy, Encode, Decode, Hash, Eq, PartialEq, Default, PartialOrd, Ord)]
|
||||||
#[cfg_attr(feature = "std", derive(MallocSizeOf))]
|
#[cfg_attr(feature = "std", derive(MallocSizeOf))]
|
||||||
pub struct CandidateHash(pub Hash);
|
pub struct CandidateHash(pub Hash);
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,11 @@ use polkadot_node_subsystem::{
|
|||||||
},
|
},
|
||||||
SubsystemContext, SubsystemError, SubsystemResult,
|
SubsystemContext, SubsystemError, SubsystemResult,
|
||||||
};
|
};
|
||||||
|
use polkadot_node_subsystem_util::rolling_session_window::{
|
||||||
|
RollingSessionWindow, SessionWindowUpdate,
|
||||||
|
};
|
||||||
use polkadot_primitives::v1::{
|
use polkadot_primitives::v1::{
|
||||||
Hash, SessionIndex, SessionInfo, CandidateEvent, Header, CandidateHash,
|
Hash, SessionIndex, CandidateEvent, Header, CandidateHash,
|
||||||
CandidateReceipt, CoreIndex, GroupIndex, BlockNumber, ConsensusLog,
|
CandidateReceipt, CoreIndex, GroupIndex, BlockNumber, ConsensusLog,
|
||||||
};
|
};
|
||||||
use polkadot_node_primitives::approval::{
|
use polkadot_node_primitives::approval::{
|
||||||
@@ -58,32 +61,7 @@ use crate::persisted_entries::CandidateEntry;
|
|||||||
use crate::criteria::{AssignmentCriteria, OurAssignment};
|
use crate::criteria::{AssignmentCriteria, OurAssignment};
|
||||||
use crate::time::{slot_number_to_tick, Tick};
|
use crate::time::{slot_number_to_tick, Tick};
|
||||||
|
|
||||||
use super::{APPROVAL_SESSIONS, LOG_TARGET, State, DBReader};
|
use super::{LOG_TARGET, State, DBReader};
|
||||||
|
|
||||||
/// A rolling window of sessions.
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct RollingSessionWindow {
|
|
||||||
pub earliest_session: Option<SessionIndex>,
|
|
||||||
pub session_info: Vec<SessionInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RollingSessionWindow {
|
|
||||||
pub fn session_info(&self, index: SessionIndex) -> Option<&SessionInfo> {
|
|
||||||
self.earliest_session.and_then(|earliest| {
|
|
||||||
if index < earliest {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
self.session_info.get((index - earliest) as usize)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn latest_session(&self) -> Option<SessionIndex> {
|
|
||||||
self.earliest_session
|
|
||||||
.map(|earliest| earliest + (self.session_info.len() as SessionIndex).saturating_sub(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given a new chain-head hash, this determines the hashes of all new blocks we should track
|
// Given a new chain-head hash, this determines the hashes of all new blocks we should track
|
||||||
// metadata for, given this head. The list will typically include the `head` hash provided unless
|
// metadata for, given this head. The list will typically include the `head` hash provided unless
|
||||||
@@ -191,153 +169,6 @@ async fn determine_new_blocks(
|
|||||||
Ok(ancestry)
|
Ok(ancestry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sessions unavailable in state to cache.
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct SessionsUnavailable;
|
|
||||||
|
|
||||||
async fn load_all_sessions(
|
|
||||||
ctx: &mut impl SubsystemContext,
|
|
||||||
block_hash: Hash,
|
|
||||||
start: SessionIndex,
|
|
||||||
end_inclusive: SessionIndex,
|
|
||||||
) -> Result<Vec<SessionInfo>, SessionsUnavailable> {
|
|
||||||
let mut v = Vec::new();
|
|
||||||
for i in start..=end_inclusive {
|
|
||||||
let (tx, rx)= oneshot::channel();
|
|
||||||
ctx.send_message(RuntimeApiMessage::Request(
|
|
||||||
block_hash,
|
|
||||||
RuntimeApiRequest::SessionInfo(i, tx),
|
|
||||||
).into()).await;
|
|
||||||
|
|
||||||
let session_info = match rx.await {
|
|
||||||
Ok(Ok(Some(s))) => s,
|
|
||||||
Ok(Ok(None)) => {
|
|
||||||
tracing::debug!(
|
|
||||||
target: LOG_TARGET,
|
|
||||||
"Session {} is missing from session-info state of block {}",
|
|
||||||
i,
|
|
||||||
block_hash,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Err(SessionsUnavailable);
|
|
||||||
}
|
|
||||||
Ok(Err(_)) | Err(_) => return Err(SessionsUnavailable),
|
|
||||||
};
|
|
||||||
|
|
||||||
v.push(session_info);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// When inspecting a new import notification, updates the session info cache to match
|
|
||||||
// the session of the imported block.
|
|
||||||
//
|
|
||||||
// this only needs to be called on heads where we are directly notified about import, as sessions do
|
|
||||||
// not change often and import notifications are expected to be typically increasing in session number.
|
|
||||||
//
|
|
||||||
// some backwards drift in session index is acceptable.
|
|
||||||
async fn cache_session_info_for_head(
|
|
||||||
ctx: &mut impl SubsystemContext,
|
|
||||||
session_window: &mut RollingSessionWindow,
|
|
||||||
block_hash: Hash,
|
|
||||||
block_header: &Header,
|
|
||||||
) -> Result<(), SessionsUnavailable> {
|
|
||||||
let session_index = {
|
|
||||||
let (s_tx, s_rx) = oneshot::channel();
|
|
||||||
|
|
||||||
// The genesis is guaranteed to be at the beginning of the session and its parent state
|
|
||||||
// is non-existent. Therefore if we're at the genesis, we request using its state and
|
|
||||||
// not the parent.
|
|
||||||
ctx.send_message(RuntimeApiMessage::Request(
|
|
||||||
if block_header.number == 0 { block_hash } else { block_header.parent_hash },
|
|
||||||
RuntimeApiRequest::SessionIndexForChild(s_tx),
|
|
||||||
).into()).await;
|
|
||||||
|
|
||||||
match s_rx.await {
|
|
||||||
Ok(Ok(s)) => s,
|
|
||||||
Ok(Err(_)) | Err(_) => return Err(SessionsUnavailable),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match session_window.earliest_session {
|
|
||||||
None => {
|
|
||||||
// First block processed on start-up.
|
|
||||||
|
|
||||||
let window_start = session_index.saturating_sub(APPROVAL_SESSIONS - 1);
|
|
||||||
|
|
||||||
tracing::debug!(
|
|
||||||
target: LOG_TARGET, "Loading approval window from session {}..={}",
|
|
||||||
window_start, session_index,
|
|
||||||
);
|
|
||||||
|
|
||||||
match load_all_sessions(ctx, block_hash, window_start, session_index).await {
|
|
||||||
Err(SessionsUnavailable) => {
|
|
||||||
tracing::debug!(
|
|
||||||
target: LOG_TARGET,
|
|
||||||
"Could not load sessions {}..={} from block {:?} in session {}",
|
|
||||||
window_start, session_index, block_hash, session_index,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Err(SessionsUnavailable);
|
|
||||||
},
|
|
||||||
Ok(s) => {
|
|
||||||
session_window.earliest_session = Some(window_start);
|
|
||||||
session_window.session_info = s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(old_window_start) => {
|
|
||||||
let latest = session_window.latest_session().expect("latest always exists if earliest does; qed");
|
|
||||||
|
|
||||||
// Either cached or ancient.
|
|
||||||
if session_index <= latest { return Ok(()) }
|
|
||||||
|
|
||||||
let old_window_end = latest;
|
|
||||||
|
|
||||||
let window_start = session_index.saturating_sub(APPROVAL_SESSIONS - 1);
|
|
||||||
tracing::info!(
|
|
||||||
target: LOG_TARGET, "Moving approval window from session {}..={} to {}..={}",
|
|
||||||
old_window_start, old_window_end,
|
|
||||||
window_start, session_index,
|
|
||||||
);
|
|
||||||
|
|
||||||
// keep some of the old window, if applicable.
|
|
||||||
let overlap_start = window_start.saturating_sub(old_window_start);
|
|
||||||
|
|
||||||
let fresh_start = if latest < window_start {
|
|
||||||
window_start
|
|
||||||
} else {
|
|
||||||
latest + 1
|
|
||||||
};
|
|
||||||
|
|
||||||
match load_all_sessions(ctx, block_hash, fresh_start, session_index).await {
|
|
||||||
Err(SessionsUnavailable) => {
|
|
||||||
tracing::warn!(
|
|
||||||
target: LOG_TARGET,
|
|
||||||
"Could not load sessions {}..={} from block {:?} in session {}",
|
|
||||||
latest + 1, session_index, block_hash, session_index,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Err(SessionsUnavailable);
|
|
||||||
}
|
|
||||||
Ok(s) => {
|
|
||||||
let outdated = std::cmp::min(overlap_start as usize, session_window.session_info.len());
|
|
||||||
session_window.session_info.drain(..outdated);
|
|
||||||
session_window.session_info.extend(s);
|
|
||||||
// we need to account for this case:
|
|
||||||
// window_start ................................... session_index
|
|
||||||
// old_window_start ........... latest
|
|
||||||
let new_earliest = std::cmp::max(window_start, old_window_start);
|
|
||||||
session_window.earliest_session = Some(new_earliest);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ImportedBlockInfo {
|
struct ImportedBlockInfo {
|
||||||
included_candidates: Vec<(CandidateHash, CandidateReceipt, CoreIndex, GroupIndex)>,
|
included_candidates: Vec<(CandidateHash, CandidateReceipt, CoreIndex, GroupIndex)>,
|
||||||
session_index: SessionIndex,
|
session_index: SessionIndex,
|
||||||
@@ -401,7 +232,7 @@ async fn imported_block_info(
|
|||||||
Err(_) => return Ok(None),
|
Err(_) => return Ok(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
if env.session_window.earliest_session.as_ref().map_or(true, |e| &session_index < e) {
|
if env.session_window.earliest_session().map_or(true, |e| session_index < e) {
|
||||||
tracing::debug!(target: LOG_TARGET, "Block {} is from ancient session {}. Skipping",
|
tracing::debug!(target: LOG_TARGET, "Block {} is from ancient session {}. Skipping",
|
||||||
block_hash, session_index);
|
block_hash, session_index);
|
||||||
|
|
||||||
@@ -591,21 +422,25 @@ pub(crate) async fn handle_new_head(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(SessionsUnavailable)
|
match state.session_window.cache_session_info_for_head(ctx, head, &header).await {
|
||||||
= cache_session_info_for_head(
|
Err(e) => {
|
||||||
ctx,
|
tracing::warn!(
|
||||||
&mut state.session_window,
|
target: LOG_TARGET,
|
||||||
head,
|
?head,
|
||||||
&header,
|
?e,
|
||||||
).await
|
"Could not cache session info when processing head.",
|
||||||
{
|
);
|
||||||
tracing::debug!(
|
|
||||||
target: LOG_TARGET,
|
|
||||||
"Could not cache session info when processing head {:?}",
|
|
||||||
head,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Ok(Vec::new())
|
return Ok(Vec::new())
|
||||||
|
}
|
||||||
|
Ok(a @ SessionWindowUpdate::Advanced { .. }) => {
|
||||||
|
tracing::info!(
|
||||||
|
target: LOG_TARGET,
|
||||||
|
update = ?a,
|
||||||
|
"Advanced session window for approvals",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've just started the node and haven't yet received any finality notifications,
|
// If we've just started the node and haven't yet received any finality notifications,
|
||||||
@@ -815,7 +650,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use polkadot_node_subsystem_test_helpers::make_subsystem_context;
|
use polkadot_node_subsystem_test_helpers::make_subsystem_context;
|
||||||
use polkadot_node_primitives::approval::{VRFOutput, VRFProof};
|
use polkadot_node_primitives::approval::{VRFOutput, VRFProof};
|
||||||
use polkadot_primitives::v1::ValidatorIndex;
|
use polkadot_primitives::v1::{SessionInfo, ValidatorIndex};
|
||||||
use polkadot_node_subsystem::messages::AllMessages;
|
use polkadot_node_subsystem::messages::AllMessages;
|
||||||
use sp_core::testing::TaskExecutor;
|
use sp_core::testing::TaskExecutor;
|
||||||
use sp_runtime::{Digest, DigestItem};
|
use sp_runtime::{Digest, DigestItem};
|
||||||
@@ -828,7 +663,7 @@ mod tests {
|
|||||||
use merlin::Transcript;
|
use merlin::Transcript;
|
||||||
use std::{pin::Pin, sync::Arc};
|
use std::{pin::Pin, sync::Arc};
|
||||||
|
|
||||||
use crate::{criteria, BlockEntry};
|
use crate::{APPROVAL_SESSIONS, criteria, BlockEntry};
|
||||||
|
|
||||||
const DATA_COL: u32 = 0;
|
const DATA_COL: u32 = 0;
|
||||||
const NUM_COLUMNS: u32 = 1;
|
const NUM_COLUMNS: u32 = 1;
|
||||||
@@ -884,7 +719,7 @@ mod tests {
|
|||||||
|
|
||||||
fn blank_state() -> State<TestDB> {
|
fn blank_state() -> State<TestDB> {
|
||||||
State {
|
State {
|
||||||
session_window: RollingSessionWindow::default(),
|
session_window: RollingSessionWindow::new(APPROVAL_SESSIONS),
|
||||||
keystore: Arc::new(LocalKeystore::in_memory()),
|
keystore: Arc::new(LocalKeystore::in_memory()),
|
||||||
slot_duration_millis: 6_000,
|
slot_duration_millis: 6_000,
|
||||||
db: TestDB::default(),
|
db: TestDB::default(),
|
||||||
@@ -897,10 +732,11 @@ mod tests {
|
|||||||
-> State<TestDB>
|
-> State<TestDB>
|
||||||
{
|
{
|
||||||
State {
|
State {
|
||||||
session_window: RollingSessionWindow {
|
session_window: RollingSessionWindow::with_session_info(
|
||||||
earliest_session: Some(index),
|
APPROVAL_SESSIONS,
|
||||||
session_info: vec![info],
|
index,
|
||||||
},
|
vec![info],
|
||||||
|
),
|
||||||
..blank_state()
|
..blank_state()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1423,14 +1259,11 @@ mod tests {
|
|||||||
.map(|(r, c, g)| (r.hash(), r.clone(), *c, *g))
|
.map(|(r, c, g)| (r.hash(), r.clone(), *c, *g))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let session_window = {
|
let session_window = RollingSessionWindow::with_session_info(
|
||||||
let mut window = RollingSessionWindow::default();
|
APPROVAL_SESSIONS,
|
||||||
|
session,
|
||||||
window.earliest_session = Some(session);
|
vec![session_info],
|
||||||
window.session_info.push(session_info);
|
);
|
||||||
|
|
||||||
window
|
|
||||||
};
|
|
||||||
|
|
||||||
let header = header.clone();
|
let header = header.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -1537,14 +1370,11 @@ mod tests {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let test_fut = {
|
let test_fut = {
|
||||||
let session_window = {
|
let session_window = RollingSessionWindow::with_session_info(
|
||||||
let mut window = RollingSessionWindow::default();
|
APPROVAL_SESSIONS,
|
||||||
|
session,
|
||||||
window.earliest_session = Some(session);
|
vec![session_info],
|
||||||
window.session_info.push(session_info);
|
);
|
||||||
|
|
||||||
window
|
|
||||||
};
|
|
||||||
|
|
||||||
let header = header.clone();
|
let header = header.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -1645,7 +1475,7 @@ mod tests {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let test_fut = {
|
let test_fut = {
|
||||||
let session_window = RollingSessionWindow::default();
|
let session_window = RollingSessionWindow::new(APPROVAL_SESSIONS);
|
||||||
|
|
||||||
let header = header.clone();
|
let header = header.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -1748,14 +1578,11 @@ mod tests {
|
|||||||
.map(|(r, c, g)| (r.hash(), r.clone(), *c, *g))
|
.map(|(r, c, g)| (r.hash(), r.clone(), *c, *g))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let session_window = {
|
let session_window = RollingSessionWindow::with_session_info(
|
||||||
let mut window = RollingSessionWindow::default();
|
APPROVAL_SESSIONS,
|
||||||
|
session,
|
||||||
window.earliest_session = Some(session);
|
vec![session_info],
|
||||||
window.session_info.push(session_info);
|
);
|
||||||
|
|
||||||
window
|
|
||||||
};
|
|
||||||
|
|
||||||
let header = header.clone();
|
let header = header.clone();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -2019,318 +1846,4 @@ mod tests {
|
|||||||
|
|
||||||
futures::executor::block_on(futures::future::join(test_fut, aux_fut));
|
futures::executor::block_on(futures::future::join(test_fut, aux_fut));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cache_session_info_test(
|
|
||||||
expected_start_session: SessionIndex,
|
|
||||||
session: SessionIndex,
|
|
||||||
mut window: RollingSessionWindow,
|
|
||||||
expect_requests_from: SessionIndex,
|
|
||||||
) {
|
|
||||||
let header = Header {
|
|
||||||
digest: Digest::default(),
|
|
||||||
extrinsics_root: Default::default(),
|
|
||||||
number: 5,
|
|
||||||
state_root: Default::default(),
|
|
||||||
parent_hash: Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let pool = TaskExecutor::new();
|
|
||||||
let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone());
|
|
||||||
|
|
||||||
let hash = header.hash();
|
|
||||||
|
|
||||||
let test_fut = {
|
|
||||||
let header = header.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
cache_session_info_for_head(
|
|
||||||
&mut ctx,
|
|
||||||
&mut window,
|
|
||||||
hash,
|
|
||||||
&header,
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(window.earliest_session, Some(expected_start_session));
|
|
||||||
assert_eq!(
|
|
||||||
window.session_info,
|
|
||||||
(expected_start_session..=session).map(dummy_session_info).collect::<Vec<_>>(),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let aux_fut = Box::pin(async move {
|
|
||||||
assert_matches!(
|
|
||||||
handle.recv().await,
|
|
||||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
|
||||||
h,
|
|
||||||
RuntimeApiRequest::SessionIndexForChild(s_tx),
|
|
||||||
)) => {
|
|
||||||
assert_eq!(h, header.parent_hash);
|
|
||||||
let _ = s_tx.send(Ok(session));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
for i in expect_requests_from..=session {
|
|
||||||
assert_matches!(
|
|
||||||
handle.recv().await,
|
|
||||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
|
||||||
h,
|
|
||||||
RuntimeApiRequest::SessionInfo(j, s_tx),
|
|
||||||
)) => {
|
|
||||||
assert_eq!(h, hash);
|
|
||||||
assert_eq!(i, j);
|
|
||||||
let _ = s_tx.send(Ok(Some(dummy_session_info(i))));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
futures::executor::block_on(futures::future::join(test_fut, aux_fut));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cache_session_info_first_early() {
|
|
||||||
cache_session_info_test(
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
RollingSessionWindow::default(),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cache_session_info_does_not_underflow() {
|
|
||||||
let window = RollingSessionWindow {
|
|
||||||
earliest_session: Some(1),
|
|
||||||
session_info: vec![dummy_session_info(1)],
|
|
||||||
};
|
|
||||||
|
|
||||||
cache_session_info_test(
|
|
||||||
1,
|
|
||||||
2,
|
|
||||||
window,
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cache_session_info_first_late() {
|
|
||||||
cache_session_info_test(
|
|
||||||
(100 as SessionIndex).saturating_sub(APPROVAL_SESSIONS - 1),
|
|
||||||
100,
|
|
||||||
RollingSessionWindow::default(),
|
|
||||||
(100 as SessionIndex).saturating_sub(APPROVAL_SESSIONS - 1),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cache_session_info_jump() {
|
|
||||||
let window = RollingSessionWindow {
|
|
||||||
earliest_session: Some(50),
|
|
||||||
session_info: vec![dummy_session_info(50), dummy_session_info(51), dummy_session_info(52)],
|
|
||||||
};
|
|
||||||
|
|
||||||
cache_session_info_test(
|
|
||||||
(100 as SessionIndex).saturating_sub(APPROVAL_SESSIONS - 1),
|
|
||||||
100,
|
|
||||||
window,
|
|
||||||
(100 as SessionIndex).saturating_sub(APPROVAL_SESSIONS - 1),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cache_session_info_roll_full() {
|
|
||||||
let start = 99 - (APPROVAL_SESSIONS - 1);
|
|
||||||
let window = RollingSessionWindow {
|
|
||||||
earliest_session: Some(start),
|
|
||||||
session_info: (start..=99).map(dummy_session_info).collect(),
|
|
||||||
};
|
|
||||||
|
|
||||||
cache_session_info_test(
|
|
||||||
(100 as SessionIndex).saturating_sub(APPROVAL_SESSIONS - 1),
|
|
||||||
100,
|
|
||||||
window,
|
|
||||||
100, // should only make one request.
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cache_session_info_roll_many_full() {
|
|
||||||
let start = 97 - (APPROVAL_SESSIONS - 1);
|
|
||||||
let window = RollingSessionWindow {
|
|
||||||
earliest_session: Some(start),
|
|
||||||
session_info: (start..=97).map(dummy_session_info).collect(),
|
|
||||||
};
|
|
||||||
|
|
||||||
cache_session_info_test(
|
|
||||||
(100 as SessionIndex).saturating_sub(APPROVAL_SESSIONS - 1),
|
|
||||||
100,
|
|
||||||
window,
|
|
||||||
98,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cache_session_info_roll_early() {
|
|
||||||
let start = 0;
|
|
||||||
let window = RollingSessionWindow {
|
|
||||||
earliest_session: Some(start),
|
|
||||||
session_info: (0..=1).map(dummy_session_info).collect(),
|
|
||||||
};
|
|
||||||
|
|
||||||
cache_session_info_test(
|
|
||||||
0,
|
|
||||||
2,
|
|
||||||
window,
|
|
||||||
2, // should only make one request.
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cache_session_info_roll_many_early() {
|
|
||||||
let start = 0;
|
|
||||||
let window = RollingSessionWindow {
|
|
||||||
earliest_session: Some(start),
|
|
||||||
session_info: (0..=1).map(dummy_session_info).collect(),
|
|
||||||
};
|
|
||||||
|
|
||||||
cache_session_info_test(
|
|
||||||
0,
|
|
||||||
3,
|
|
||||||
window,
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn any_session_unavailable_for_caching_means_no_change() {
|
|
||||||
let session: SessionIndex = 6;
|
|
||||||
let start_session = session.saturating_sub(APPROVAL_SESSIONS - 1);
|
|
||||||
|
|
||||||
let header = Header {
|
|
||||||
digest: Digest::default(),
|
|
||||||
extrinsics_root: Default::default(),
|
|
||||||
number: 5,
|
|
||||||
state_root: Default::default(),
|
|
||||||
parent_hash: Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let pool = TaskExecutor::new();
|
|
||||||
let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone());
|
|
||||||
|
|
||||||
let mut window = RollingSessionWindow::default();
|
|
||||||
let hash = header.hash();
|
|
||||||
|
|
||||||
let test_fut = {
|
|
||||||
let header = header.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
let res = cache_session_info_for_head(
|
|
||||||
&mut ctx,
|
|
||||||
&mut window,
|
|
||||||
hash,
|
|
||||||
&header,
|
|
||||||
).await;
|
|
||||||
|
|
||||||
assert_matches!(res, Err(SessionsUnavailable));
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let aux_fut = Box::pin(async move {
|
|
||||||
assert_matches!(
|
|
||||||
handle.recv().await,
|
|
||||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
|
||||||
h,
|
|
||||||
RuntimeApiRequest::SessionIndexForChild(s_tx),
|
|
||||||
)) => {
|
|
||||||
assert_eq!(h, header.parent_hash);
|
|
||||||
let _ = s_tx.send(Ok(session));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
for i in start_session..=session {
|
|
||||||
assert_matches!(
|
|
||||||
handle.recv().await,
|
|
||||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
|
||||||
h,
|
|
||||||
RuntimeApiRequest::SessionInfo(j, s_tx),
|
|
||||||
)) => {
|
|
||||||
assert_eq!(h, hash);
|
|
||||||
assert_eq!(i, j);
|
|
||||||
|
|
||||||
let _ = s_tx.send(Ok(if i == session {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(dummy_session_info(i))
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
futures::executor::block_on(futures::future::join(test_fut, aux_fut));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn request_session_info_for_genesis() {
|
|
||||||
let session: SessionIndex = 0;
|
|
||||||
|
|
||||||
let header = Header {
|
|
||||||
digest: Digest::default(),
|
|
||||||
extrinsics_root: Default::default(),
|
|
||||||
number: 0,
|
|
||||||
state_root: Default::default(),
|
|
||||||
parent_hash: Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let pool = TaskExecutor::new();
|
|
||||||
let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone());
|
|
||||||
|
|
||||||
let mut window = RollingSessionWindow::default();
|
|
||||||
let hash = header.hash();
|
|
||||||
|
|
||||||
let test_fut = {
|
|
||||||
let header = header.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
cache_session_info_for_head(
|
|
||||||
&mut ctx,
|
|
||||||
&mut window,
|
|
||||||
hash,
|
|
||||||
&header,
|
|
||||||
).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(window.earliest_session, Some(session));
|
|
||||||
assert_eq!(
|
|
||||||
window.session_info,
|
|
||||||
vec![dummy_session_info(session)],
|
|
||||||
);
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
let aux_fut = Box::pin(async move {
|
|
||||||
assert_matches!(
|
|
||||||
handle.recv().await,
|
|
||||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
|
||||||
h,
|
|
||||||
RuntimeApiRequest::SessionIndexForChild(s_tx),
|
|
||||||
)) => {
|
|
||||||
assert_eq!(h, hash);
|
|
||||||
let _ = s_tx.send(Ok(session));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_matches!(
|
|
||||||
handle.recv().await,
|
|
||||||
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
|
||||||
h,
|
|
||||||
RuntimeApiRequest::SessionInfo(s, s_tx),
|
|
||||||
)) => {
|
|
||||||
assert_eq!(h, hash);
|
|
||||||
assert_eq!(s, session);
|
|
||||||
|
|
||||||
let _ = s_tx.send(Ok(Some(dummy_session_info(s))));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
futures::executor::block_on(futures::future::join(test_fut, aux_fut));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,20 +33,19 @@ use polkadot_node_subsystem::{
|
|||||||
};
|
};
|
||||||
use polkadot_node_subsystem_util::{
|
use polkadot_node_subsystem_util::{
|
||||||
metrics::{self, prometheus},
|
metrics::{self, prometheus},
|
||||||
|
rolling_session_window::RollingSessionWindow,
|
||||||
};
|
};
|
||||||
use polkadot_primitives::v1::{
|
use polkadot_primitives::v1::{
|
||||||
ValidatorIndex, Hash, SessionIndex, SessionInfo, CandidateHash,
|
ValidatorIndex, Hash, SessionIndex, SessionInfo, CandidateHash,
|
||||||
CandidateReceipt, BlockNumber, PersistedValidationData,
|
CandidateReceipt, BlockNumber, PersistedValidationData,
|
||||||
ValidationCode, CandidateDescriptor, ValidatorPair, ValidatorSignature, ValidatorId,
|
ValidationCode, CandidateDescriptor, ValidatorPair, ValidatorSignature, ValidatorId,
|
||||||
CandidateIndex, GroupIndex,
|
CandidateIndex, GroupIndex, ApprovalVote,
|
||||||
};
|
};
|
||||||
use polkadot_node_primitives::{ValidationResult, PoV};
|
use polkadot_node_primitives::{ValidationResult, PoV};
|
||||||
use polkadot_node_primitives::approval::{
|
use polkadot_node_primitives::approval::{
|
||||||
IndirectAssignmentCert, IndirectSignedApprovalVote, ApprovalVote, DelayTranche,
|
IndirectAssignmentCert, IndirectSignedApprovalVote, DelayTranche, BlockApprovalMeta,
|
||||||
BlockApprovalMeta,
|
|
||||||
};
|
};
|
||||||
use polkadot_node_jaeger as jaeger;
|
use polkadot_node_jaeger as jaeger;
|
||||||
use parity_scale_codec::Encode;
|
|
||||||
use sc_keystore::LocalKeystore;
|
use sc_keystore::LocalKeystore;
|
||||||
use sp_consensus::SyncOracle;
|
use sp_consensus::SyncOracle;
|
||||||
use sp_consensus_slots::Slot;
|
use sp_consensus_slots::Slot;
|
||||||
@@ -497,7 +496,7 @@ struct ApprovalStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct State<T> {
|
struct State<T> {
|
||||||
session_window: import::RollingSessionWindow,
|
session_window: RollingSessionWindow,
|
||||||
keystore: Arc<LocalKeystore>,
|
keystore: Arc<LocalKeystore>,
|
||||||
slot_duration_millis: u64,
|
slot_duration_millis: u64,
|
||||||
db: T,
|
db: T,
|
||||||
@@ -591,7 +590,7 @@ async fn run<C>(
|
|||||||
{
|
{
|
||||||
let (background_tx, background_rx) = mpsc::channel::<BackgroundRequest>(64);
|
let (background_tx, background_rx) = mpsc::channel::<BackgroundRequest>(64);
|
||||||
let mut state = State {
|
let mut state = State {
|
||||||
session_window: Default::default(),
|
session_window: RollingSessionWindow::new(APPROVAL_SESSIONS),
|
||||||
keystore: subsystem.keystore,
|
keystore: subsystem.keystore,
|
||||||
slot_duration_millis: subsystem.slot_duration_millis,
|
slot_duration_millis: subsystem.slot_duration_millis,
|
||||||
db: ApprovalDBV1Reader::new(subsystem.db.clone(), subsystem.db_config.clone()),
|
db: ApprovalDBV1Reader::new(subsystem.db.clone(), subsystem.db_config.clone()),
|
||||||
@@ -1226,15 +1225,6 @@ async fn handle_approved_ancestor(
|
|||||||
Ok(all_approved_max)
|
Ok(all_approved_max)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn approval_signing_payload(
|
|
||||||
approval_vote: ApprovalVote,
|
|
||||||
session_index: SessionIndex,
|
|
||||||
) -> Vec<u8> {
|
|
||||||
const MAGIC: [u8; 4] = *b"APPR";
|
|
||||||
|
|
||||||
(MAGIC, approval_vote, session_index).encode()
|
|
||||||
}
|
|
||||||
|
|
||||||
// `Option::cmp` treats `None` as less than `Some`.
|
// `Option::cmp` treats `None` as less than `Some`.
|
||||||
fn min_prefer_some<T: std::cmp::Ord>(
|
fn min_prefer_some<T: std::cmp::Ord>(
|
||||||
a: Option<T>,
|
a: Option<T>,
|
||||||
@@ -1466,10 +1456,8 @@ fn check_and_import_approval<T>(
|
|||||||
None => respond_early!(ApprovalCheckResult::Bad)
|
None => respond_early!(ApprovalCheckResult::Bad)
|
||||||
};
|
};
|
||||||
|
|
||||||
let approval_payload = approval_signing_payload(
|
let approval_payload = ApprovalVote(approved_candidate_hash)
|
||||||
ApprovalVote(approved_candidate_hash),
|
.signing_payload(block_entry.session());
|
||||||
block_entry.session(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let pubkey = match session_info.validators.get(approval.validator.0 as usize) {
|
let pubkey = match session_info.validators.get(approval.validator.0 as usize) {
|
||||||
Some(k) => k,
|
Some(k) => k,
|
||||||
@@ -2147,10 +2135,7 @@ fn sign_approval(
|
|||||||
) -> Option<ValidatorSignature> {
|
) -> Option<ValidatorSignature> {
|
||||||
let key = keystore.key_pair::<ValidatorPair>(public).ok().flatten()?;
|
let key = keystore.key_pair::<ValidatorPair>(public).ok().flatten()?;
|
||||||
|
|
||||||
let payload = approval_signing_payload(
|
let payload = ApprovalVote(candidate_hash).signing_payload(session_index);
|
||||||
ApprovalVote(candidate_hash),
|
|
||||||
session_index,
|
|
||||||
);
|
|
||||||
|
|
||||||
Some(key.sign(&payload[..]))
|
Some(key.sign(&payload[..]))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ impl DBReader for TestStore {
|
|||||||
|
|
||||||
fn blank_state() -> State<TestStore> {
|
fn blank_state() -> State<TestStore> {
|
||||||
State {
|
State {
|
||||||
session_window: import::RollingSessionWindow::default(),
|
session_window: RollingSessionWindow::new(APPROVAL_SESSIONS),
|
||||||
keystore: Arc::new(LocalKeystore::in_memory()),
|
keystore: Arc::new(LocalKeystore::in_memory()),
|
||||||
slot_duration_millis: SLOT_DURATION_MILLIS,
|
slot_duration_millis: SLOT_DURATION_MILLIS,
|
||||||
db: TestStore::default(),
|
db: TestStore::default(),
|
||||||
@@ -204,10 +204,11 @@ fn single_session_state(index: SessionIndex, info: SessionInfo)
|
|||||||
-> State<TestStore>
|
-> State<TestStore>
|
||||||
{
|
{
|
||||||
State {
|
State {
|
||||||
session_window: import::RollingSessionWindow {
|
session_window: RollingSessionWindow::with_session_info(
|
||||||
earliest_session: Some(index),
|
APPROVAL_SESSIONS,
|
||||||
session_info: vec![info],
|
index,
|
||||||
},
|
vec![info],
|
||||||
|
),
|
||||||
..blank_state()
|
..blank_state()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +232,7 @@ fn sign_approval(
|
|||||||
candidate_hash: CandidateHash,
|
candidate_hash: CandidateHash,
|
||||||
session_index: SessionIndex,
|
session_index: SessionIndex,
|
||||||
) -> ValidatorSignature {
|
) -> ValidatorSignature {
|
||||||
key.sign(&super::approval_signing_payload(ApprovalVote(candidate_hash), session_index)).into()
|
key.sign(&ApprovalVote(candidate_hash).signing_payload(session_index)).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[package]
|
||||||
|
name = "polkadot-node-core-dispute-coordinator"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Parity Technologies <admin@parity.io>"]
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bitvec = { version = "0.20.1", default-features = false, features = ["alloc"] }
|
||||||
|
futures = "0.3.12"
|
||||||
|
tracing = "0.1.26"
|
||||||
|
parity-scale-codec = "2"
|
||||||
|
kvdb = "0.9.0"
|
||||||
|
derive_more = "0.99.1"
|
||||||
|
thiserror = "1.0.23"
|
||||||
|
|
||||||
|
polkadot-primitives = { path = "../../../primitives" }
|
||||||
|
polkadot-node-primitives = { path = "../../primitives" }
|
||||||
|
polkadot-node-subsystem = { path = "../../subsystem" }
|
||||||
|
polkadot-node-subsystem-util = { path = "../../subsystem-util" }
|
||||||
|
|
||||||
|
sc-keystore = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
kvdb-memorydb = "0.9.0"
|
||||||
|
polkadot-node-subsystem-test-helpers = { path = "../../subsystem-test-helpers"}
|
||||||
|
sp-keyring = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||||
|
sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||||
|
sp-keystore = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||||
|
assert_matches = "1.4.0"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright 2020 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Polkadot.
|
||||||
|
|
||||||
|
// Polkadot is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Polkadot is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//! Database component for the dispute coordinator.
|
||||||
|
|
||||||
|
pub(super) mod v1;
|
||||||
@@ -0,0 +1,585 @@
|
|||||||
|
// Copyright 2020 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Polkadot.
|
||||||
|
|
||||||
|
// Polkadot is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Polkadot is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//! V1 database for the dispute coordinator.
|
||||||
|
|
||||||
|
use polkadot_primitives::v1::{
|
||||||
|
CandidateReceipt, ValidDisputeStatementKind, InvalidDisputeStatementKind, ValidatorIndex,
|
||||||
|
ValidatorSignature, SessionIndex, CandidateHash,
|
||||||
|
};
|
||||||
|
|
||||||
|
use kvdb::{KeyValueDB, DBTransaction};
|
||||||
|
use parity_scale_codec::{Encode, Decode};
|
||||||
|
|
||||||
|
use crate::DISPUTE_WINDOW;
|
||||||
|
|
||||||
|
const ACTIVE_DISPUTES_KEY: &[u8; 15] = b"active-disputes";
|
||||||
|
const EARLIEST_SESSION_KEY: &[u8; 16] = b"earliest-session";
|
||||||
|
const CANDIDATE_VOTES_SUBKEY: &[u8; 15] = b"candidate-votes";
|
||||||
|
|
||||||
|
fn candidate_votes_key(session: SessionIndex, candidate_hash: &CandidateHash) -> [u8; 15 + 4 + 32] {
|
||||||
|
let mut buf = [0u8; 15 + 4 + 32];
|
||||||
|
buf[..15].copy_from_slice(CANDIDATE_VOTES_SUBKEY);
|
||||||
|
|
||||||
|
// big-endian encoding is used to ensure lexicographic ordering.
|
||||||
|
buf[15..][..4].copy_from_slice(&session.to_be_bytes());
|
||||||
|
candidate_hash.using_encoded(|s| buf[(15 + 4)..].copy_from_slice(s));
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes the upper lexicographic bound on DB keys for candidate votes with a given
|
||||||
|
// upper-exclusive bound on sessions.
|
||||||
|
fn candidate_votes_range_upper_bound(upper_exclusive: SessionIndex) -> [u8; 15 + 4] {
|
||||||
|
let mut buf = [0; 15 + 4];
|
||||||
|
buf[..15].copy_from_slice(CANDIDATE_VOTES_SUBKEY);
|
||||||
|
// big-endian encoding is used to ensure lexicographic ordering.
|
||||||
|
buf[15..][..4].copy_from_slice(&upper_exclusive.to_be_bytes());
|
||||||
|
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_candidate_votes_key(key: &[u8]) -> Option<(SessionIndex, CandidateHash)> {
|
||||||
|
if key.len() != 15 + 4 + 32 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut session_buf = [0; 4];
|
||||||
|
session_buf.copy_from_slice(&key[15..][..4]);
|
||||||
|
let session = SessionIndex::from_be_bytes(session_buf);
|
||||||
|
|
||||||
|
CandidateHash::decode(&mut &key[(15 + 4)..]).ok().map(|hash| (session, hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Column configuration information for the DB.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ColumnConfiguration {
|
||||||
|
/// The column in the key-value DB where data is stored.
|
||||||
|
pub col_data: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tracked votes on candidates, for the purposes of dispute resolution.
|
||||||
|
#[derive(Debug, Clone, Encode, Decode)]
|
||||||
|
pub struct CandidateVotes {
|
||||||
|
/// The receipt of the candidate itself.
|
||||||
|
pub candidate_receipt: CandidateReceipt,
|
||||||
|
/// Votes of validity, sorted by validator index.
|
||||||
|
pub valid: Vec<(ValidDisputeStatementKind, ValidatorIndex, ValidatorSignature)>,
|
||||||
|
/// Votes of invalidity, sorted by validator index.
|
||||||
|
pub invalid: Vec<(InvalidDisputeStatementKind, ValidatorIndex, ValidatorSignature)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CandidateVotes> for polkadot_node_primitives::CandidateVotes {
|
||||||
|
fn from(db_votes: CandidateVotes) -> polkadot_node_primitives::CandidateVotes {
|
||||||
|
polkadot_node_primitives::CandidateVotes {
|
||||||
|
candidate_receipt: db_votes.candidate_receipt,
|
||||||
|
valid: db_votes.valid,
|
||||||
|
invalid: db_votes.invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<polkadot_node_primitives::CandidateVotes> for CandidateVotes {
|
||||||
|
fn from(primitive_votes: polkadot_node_primitives::CandidateVotes) -> CandidateVotes {
|
||||||
|
CandidateVotes {
|
||||||
|
candidate_receipt: primitive_votes.candidate_receipt,
|
||||||
|
valid: primitive_votes.valid,
|
||||||
|
invalid: primitive_votes.invalid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Meta-key for tracking active disputes.
|
||||||
|
#[derive(Debug, Default, Clone, Encode, Decode, PartialEq)]
|
||||||
|
pub struct ActiveDisputes {
|
||||||
|
/// All disputed candidates, sorted by session index and then by candidate hash.
|
||||||
|
pub disputed: Vec<(SessionIndex, CandidateHash)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveDisputes {
|
||||||
|
/// Whether the set of active disputes contains the given candidate.
|
||||||
|
pub(crate) fn contains(
|
||||||
|
&self,
|
||||||
|
session: SessionIndex,
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
) -> bool {
|
||||||
|
self.disputed.contains(&(session, candidate_hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert the session and candidate hash from the set of active disputes.
|
||||||
|
/// Returns 'true' if the entry was not already in the set.
|
||||||
|
pub(crate) fn insert(
|
||||||
|
&mut self,
|
||||||
|
session: SessionIndex,
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
) -> bool {
|
||||||
|
let new_entry = (session, candidate_hash);
|
||||||
|
|
||||||
|
let pos = self.disputed.iter()
|
||||||
|
.take_while(|&e| &new_entry < e)
|
||||||
|
.count();
|
||||||
|
if self.disputed.get(pos).map_or(false, |&e| new_entry == e) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
self.disputed.insert(pos, new_entry);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete the session and candidate hash from the set of active disputes.
|
||||||
|
/// Returns 'true' if the entry was present.
|
||||||
|
pub(crate) fn delete(
|
||||||
|
&mut self,
|
||||||
|
session: SessionIndex,
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
) -> bool {
|
||||||
|
let new_entry = (session, candidate_hash);
|
||||||
|
|
||||||
|
match self.disputed.iter().position(|e| &new_entry == e) {
|
||||||
|
None => false,
|
||||||
|
Some(pos) => {
|
||||||
|
self.disputed.remove(pos);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors while accessing things from the DB.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Codec(#[from] parity_scale_codec::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result alias for DB errors.
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
fn load_decode<D: Decode>(db: &dyn KeyValueDB, col_data: u32, key: &[u8])
|
||||||
|
-> Result<Option<D>>
|
||||||
|
{
|
||||||
|
match db.get(col_data, key)? {
|
||||||
|
None => Ok(None),
|
||||||
|
Some(raw) => D::decode(&mut &raw[..])
|
||||||
|
.map(Some)
|
||||||
|
.map_err(Into::into),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the candidate votes for the identified candidate under the given hash.
|
||||||
|
pub(crate) fn load_candidate_votes(
|
||||||
|
db: &dyn KeyValueDB,
|
||||||
|
config: &ColumnConfiguration,
|
||||||
|
session: SessionIndex,
|
||||||
|
candidate_hash: &CandidateHash,
|
||||||
|
) -> Result<Option<CandidateVotes>> {
|
||||||
|
load_decode(db, config.col_data, &candidate_votes_key(session, candidate_hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the earliest session, if any.
|
||||||
|
pub(crate) fn load_earliest_session(
|
||||||
|
db: &dyn KeyValueDB,
|
||||||
|
config: &ColumnConfiguration,
|
||||||
|
) -> Result<Option<SessionIndex>> {
|
||||||
|
load_decode(db, config.col_data, EARLIEST_SESSION_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the active disputes, if any.
|
||||||
|
pub(crate) fn load_active_disputes(
|
||||||
|
db: &dyn KeyValueDB,
|
||||||
|
config: &ColumnConfiguration,
|
||||||
|
) -> Result<Option<ActiveDisputes>> {
|
||||||
|
load_decode(db, config.col_data, ACTIVE_DISPUTES_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An atomic transaction to be commited to the underlying DB.
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub(crate) struct Transaction {
|
||||||
|
earliest_session: Option<SessionIndex>,
|
||||||
|
active_disputes: Option<ActiveDisputes>,
|
||||||
|
write_candidate_votes: Vec<(SessionIndex, CandidateHash, CandidateVotes)>,
|
||||||
|
delete_candidate_votes: Vec<(SessionIndex, CandidateHash)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transaction {
|
||||||
|
/// Prepare a write to the 'earliest session' field of the DB.
|
||||||
|
///
|
||||||
|
/// Later calls to this function will override earlier ones.
|
||||||
|
pub(crate) fn put_earliest_session(&mut self, session: SessionIndex) {
|
||||||
|
self.earliest_session = Some(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare a write to the active disputes stored in the DB.
|
||||||
|
///
|
||||||
|
/// Later calls to this function will override earlier ones.
|
||||||
|
pub(crate) fn put_active_disputes(&mut self, active: ActiveDisputes) {
|
||||||
|
self.active_disputes = Some(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Prepare a write of the candidate votes under the indicated candidate.
|
||||||
|
///
|
||||||
|
/// Later calls to this function for the same candidate will override earlier ones.
|
||||||
|
/// Any calls to this function will be overridden by deletions of the same candidate.
|
||||||
|
pub(crate) fn put_candidate_votes(
|
||||||
|
&mut self,
|
||||||
|
session: SessionIndex,
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
votes: CandidateVotes,
|
||||||
|
) {
|
||||||
|
self.write_candidate_votes.push((session, candidate_hash, votes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare a deletion of the candidate votes under the indicated candidate.
|
||||||
|
///
|
||||||
|
/// Any calls to this function will override writes to the same candidate.
|
||||||
|
pub(crate) fn delete_candidate_votes(
|
||||||
|
&mut self,
|
||||||
|
session: SessionIndex,
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
) {
|
||||||
|
self.delete_candidate_votes.push((session, candidate_hash))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the transaction atomically to the DB.
|
||||||
|
pub(crate) fn write(self, db: &dyn KeyValueDB, config: &ColumnConfiguration) -> Result<()> {
|
||||||
|
let mut tx = DBTransaction::new();
|
||||||
|
|
||||||
|
if let Some(s) = self.earliest_session {
|
||||||
|
tx.put_vec(config.col_data, EARLIEST_SESSION_KEY, s.encode());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(a) = self.active_disputes {
|
||||||
|
tx.put_vec(config.col_data, ACTIVE_DISPUTES_KEY, a.encode());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (session, candidate_hash, votes) in self.write_candidate_votes {
|
||||||
|
tx.put_vec(config.col_data, &candidate_votes_key(session, &candidate_hash), votes.encode());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (session, candidate_hash) in self.delete_candidate_votes {
|
||||||
|
tx.delete(config.col_data, &candidate_votes_key(session, &candidate_hash));
|
||||||
|
}
|
||||||
|
|
||||||
|
db.write(tx).map_err(Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Maybe prune data in the DB based on the provided session index.
|
||||||
|
///
|
||||||
|
/// This is intended to be called on every block, and as such will be used to populate the DB on
|
||||||
|
/// first launch. If the on-disk data does not need to be pruned, only a single storage read
|
||||||
|
/// will be performed.
|
||||||
|
///
|
||||||
|
/// If one or more ancient sessions are pruned, all metadata on candidates within the ancient
|
||||||
|
/// session will be deleted.
|
||||||
|
pub(crate) fn note_current_session(
|
||||||
|
store: &dyn KeyValueDB,
|
||||||
|
config: &ColumnConfiguration,
|
||||||
|
current_session: SessionIndex,
|
||||||
|
) -> Result<()> {
|
||||||
|
let new_earliest = current_session.saturating_sub(DISPUTE_WINDOW);
|
||||||
|
let mut tx = Transaction::default();
|
||||||
|
|
||||||
|
match load_earliest_session(store, config)? {
|
||||||
|
None => {
|
||||||
|
// First launch - write new-earliest.
|
||||||
|
tx.put_earliest_session(new_earliest);
|
||||||
|
}
|
||||||
|
Some(prev_earliest) if new_earliest > prev_earliest => {
|
||||||
|
// Prune all data in the outdated sessions.
|
||||||
|
tx.put_earliest_session(new_earliest);
|
||||||
|
|
||||||
|
// Clear active disputes metadata.
|
||||||
|
{
|
||||||
|
let mut active_disputes = load_active_disputes(store, config)?.unwrap_or_default();
|
||||||
|
let prune_up_to = active_disputes.disputed.iter()
|
||||||
|
.take_while(|s| s.0 < new_earliest)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if prune_up_to > 0 {
|
||||||
|
let _ = active_disputes.disputed.drain(..prune_up_to);
|
||||||
|
tx.put_active_disputes(active_disputes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all candidate data with session less than the new earliest kept.
|
||||||
|
{
|
||||||
|
let end_prefix = candidate_votes_range_upper_bound(new_earliest);
|
||||||
|
|
||||||
|
store.iter_with_prefix(config.col_data, CANDIDATE_VOTES_SUBKEY)
|
||||||
|
.take_while(|(k, _)| &k[..] < &end_prefix[..])
|
||||||
|
.filter_map(|(k, _)| decode_candidate_votes_key(&k[..]))
|
||||||
|
.for_each(|(session, candidate_hash)| {
|
||||||
|
tx.delete_candidate_votes(session, candidate_hash);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
// nothing to do.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tx.write(store, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use polkadot_primitives::v1::{Hash, Id as ParaId};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn candidate_votes_key_works() {
|
||||||
|
let session = 4;
|
||||||
|
let candidate = CandidateHash(Hash::repeat_byte(0x01));
|
||||||
|
|
||||||
|
let key = candidate_votes_key(session, &candidate);
|
||||||
|
|
||||||
|
assert_eq!(&key[0..15], CANDIDATE_VOTES_SUBKEY);
|
||||||
|
assert_eq!(&key[15..19], &[0x00, 0x00, 0x00, 0x04]);
|
||||||
|
assert_eq!(&key[19..51], candidate.0.as_bytes());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
decode_candidate_votes_key(&key[..]),
|
||||||
|
Some((session, candidate)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn db_transaction() {
|
||||||
|
let store = kvdb_memorydb::create(1);
|
||||||
|
let config = ColumnConfiguration { col_data: 0 };
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut tx = Transaction::default();
|
||||||
|
|
||||||
|
tx.put_earliest_session(0);
|
||||||
|
tx.put_earliest_session(1);
|
||||||
|
|
||||||
|
tx.put_active_disputes(ActiveDisputes {
|
||||||
|
disputed: vec![
|
||||||
|
(0, CandidateHash(Hash::repeat_byte(0))),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
tx.put_active_disputes(ActiveDisputes {
|
||||||
|
disputed: vec![
|
||||||
|
(1, CandidateHash(Hash::repeat_byte(1))),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
tx.put_candidate_votes(
|
||||||
|
1,
|
||||||
|
CandidateHash(Hash::repeat_byte(1)),
|
||||||
|
CandidateVotes {
|
||||||
|
candidate_receipt: Default::default(),
|
||||||
|
valid: Vec::new(),
|
||||||
|
invalid: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
tx.put_candidate_votes(
|
||||||
|
1,
|
||||||
|
CandidateHash(Hash::repeat_byte(1)),
|
||||||
|
CandidateVotes {
|
||||||
|
candidate_receipt: {
|
||||||
|
let mut receipt = CandidateReceipt::default();
|
||||||
|
receipt.descriptor.para_id = 5.into();
|
||||||
|
|
||||||
|
receipt
|
||||||
|
},
|
||||||
|
valid: Vec::new(),
|
||||||
|
invalid: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.write(&store, &config).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that subsequent writes were written.
|
||||||
|
{
|
||||||
|
assert_eq!(
|
||||||
|
load_earliest_session(&store, &config).unwrap().unwrap(),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
load_active_disputes(&store, &config).unwrap().unwrap(),
|
||||||
|
ActiveDisputes {
|
||||||
|
disputed: vec![
|
||||||
|
(1, CandidateHash(Hash::repeat_byte(1))),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
load_candidate_votes(
|
||||||
|
&store,
|
||||||
|
&config,
|
||||||
|
1,
|
||||||
|
&CandidateHash(Hash::repeat_byte(1))
|
||||||
|
).unwrap().unwrap().candidate_receipt.descriptor.para_id,
|
||||||
|
ParaId::from(5),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn db_deletes_supersede_writes() {
|
||||||
|
let store = kvdb_memorydb::create(1);
|
||||||
|
let config = ColumnConfiguration { col_data: 0 };
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut tx = Transaction::default();
|
||||||
|
tx.put_candidate_votes(
|
||||||
|
1,
|
||||||
|
CandidateHash(Hash::repeat_byte(1)),
|
||||||
|
CandidateVotes {
|
||||||
|
candidate_receipt: Default::default(),
|
||||||
|
valid: Vec::new(),
|
||||||
|
invalid: Vec::new(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.write(&store, &config).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
load_candidate_votes(
|
||||||
|
&store,
|
||||||
|
&config,
|
||||||
|
1,
|
||||||
|
&CandidateHash(Hash::repeat_byte(1))
|
||||||
|
).unwrap().unwrap().candidate_receipt.descriptor.para_id,
|
||||||
|
ParaId::from(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut tx = Transaction::default();
|
||||||
|
tx.put_candidate_votes(
|
||||||
|
1,
|
||||||
|
CandidateHash(Hash::repeat_byte(1)),
|
||||||
|
CandidateVotes {
|
||||||
|
candidate_receipt: {
|
||||||
|
let mut receipt = CandidateReceipt::default();
|
||||||
|
receipt.descriptor.para_id = 5.into();
|
||||||
|
|
||||||
|
receipt
|
||||||
|
},
|
||||||
|
valid: Vec::new(),
|
||||||
|
invalid: Vec::new(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.delete_candidate_votes(1, CandidateHash(Hash::repeat_byte(1)));
|
||||||
|
|
||||||
|
tx.write(&store, &config).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
load_candidate_votes(
|
||||||
|
&store,
|
||||||
|
&config,
|
||||||
|
1,
|
||||||
|
&CandidateHash(Hash::repeat_byte(1))
|
||||||
|
).unwrap().is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn note_current_session_prunes_old() {
|
||||||
|
let store = kvdb_memorydb::create(1);
|
||||||
|
let config = ColumnConfiguration { col_data: 0 };
|
||||||
|
|
||||||
|
let hash_a = CandidateHash(Hash::repeat_byte(0x0a));
|
||||||
|
let hash_b = CandidateHash(Hash::repeat_byte(0x0b));
|
||||||
|
let hash_c = CandidateHash(Hash::repeat_byte(0x0c));
|
||||||
|
let hash_d = CandidateHash(Hash::repeat_byte(0x0d));
|
||||||
|
|
||||||
|
let prev_earliest_session = 0;
|
||||||
|
let new_earliest_session = 5;
|
||||||
|
let current_session = 5 + DISPUTE_WINDOW;
|
||||||
|
|
||||||
|
let very_old = 3;
|
||||||
|
let slightly_old = 4;
|
||||||
|
let very_recent = current_session - 1;
|
||||||
|
|
||||||
|
let blank_candidate_votes = || CandidateVotes {
|
||||||
|
candidate_receipt: Default::default(),
|
||||||
|
valid: Vec::new(),
|
||||||
|
invalid: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut tx = Transaction::default();
|
||||||
|
tx.put_earliest_session(prev_earliest_session);
|
||||||
|
tx.put_active_disputes(ActiveDisputes {
|
||||||
|
disputed: vec![
|
||||||
|
(very_old, hash_a),
|
||||||
|
(slightly_old, hash_b),
|
||||||
|
(new_earliest_session, hash_c),
|
||||||
|
(very_recent, hash_d),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
tx.put_candidate_votes(
|
||||||
|
very_old,
|
||||||
|
hash_a,
|
||||||
|
blank_candidate_votes(),
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.put_candidate_votes(
|
||||||
|
slightly_old,
|
||||||
|
hash_b,
|
||||||
|
blank_candidate_votes(),
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.put_candidate_votes(
|
||||||
|
new_earliest_session,
|
||||||
|
hash_c,
|
||||||
|
blank_candidate_votes(),
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.put_candidate_votes(
|
||||||
|
very_recent,
|
||||||
|
hash_d,
|
||||||
|
blank_candidate_votes(),
|
||||||
|
);
|
||||||
|
|
||||||
|
tx.write(&store, &config).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
note_current_session(&store, &config, current_session).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
load_earliest_session(&store, &config).unwrap(),
|
||||||
|
Some(new_earliest_session),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
load_active_disputes(&store, &config).unwrap().unwrap(),
|
||||||
|
ActiveDisputes {
|
||||||
|
disputed: vec![(new_earliest_session, hash_c), (very_recent, hash_d)],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(load_candidate_votes(&store, &config, very_old, &hash_a).unwrap().is_none());
|
||||||
|
assert!(load_candidate_votes(&store, &config, slightly_old, &hash_b).unwrap().is_none());
|
||||||
|
assert!(load_candidate_votes(&store, &config, new_earliest_session, &hash_c).unwrap().is_some());
|
||||||
|
assert!(load_candidate_votes(&store, &config, very_recent, &hash_d).unwrap().is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,649 @@
|
|||||||
|
// Copyright 2020 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Polkadot.
|
||||||
|
|
||||||
|
// Polkadot is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Polkadot is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//! Implements the dispute coordinator subsystem.
|
||||||
|
//!
|
||||||
|
//! This is the central subsystem of the node-side components which participate in disputes.
|
||||||
|
//! This subsystem wraps a database which tracks all statements observed by all validators over some window of sessions.
|
||||||
|
//! Votes older than this session window are pruned.
|
||||||
|
//!
|
||||||
|
//! This subsystem will be the point which produce dispute votes, either positive or negative, based on locally-observed
|
||||||
|
//! validation results as well as a sink for votes received by other subsystems. When importing a dispute vote from
|
||||||
|
//! another node, this will trigger the dispute participation subsystem to recover and validate the block and call
|
||||||
|
//! back to this subsystem.
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use polkadot_node_primitives::{CandidateVotes, SignedDisputeStatement};
|
||||||
|
use polkadot_node_subsystem::{
|
||||||
|
messages::{
|
||||||
|
DisputeCoordinatorMessage, ChainApiMessage, DisputeParticipationMessage,
|
||||||
|
},
|
||||||
|
Subsystem, SubsystemContext, FromOverseer, OverseerSignal, SpawnedSubsystem,
|
||||||
|
SubsystemError,
|
||||||
|
errors::{ChainApiError, RuntimeApiError},
|
||||||
|
};
|
||||||
|
use polkadot_node_subsystem_util::rolling_session_window::{
|
||||||
|
RollingSessionWindow, SessionWindowUpdate,
|
||||||
|
};
|
||||||
|
use polkadot_primitives::v1::{
|
||||||
|
SessionIndex, CandidateHash, Hash, CandidateReceipt, DisputeStatement, ValidatorIndex,
|
||||||
|
ValidatorSignature, BlockNumber, ValidatorPair,
|
||||||
|
};
|
||||||
|
|
||||||
|
use futures::prelude::*;
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use kvdb::KeyValueDB;
|
||||||
|
use parity_scale_codec::Error as CodecError;
|
||||||
|
use sc_keystore::LocalKeystore;
|
||||||
|
|
||||||
|
use db::v1::ActiveDisputes;
|
||||||
|
|
||||||
|
mod db;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
const LOG_TARGET: &str = "parachain::dispute-coordinator";
|
||||||
|
|
||||||
|
// It would be nice to draw this from the chain state, but we have no tools for it right now.
|
||||||
|
// On Polkadot this is 1 day, and on Kusama it's 6 hours.
|
||||||
|
const DISPUTE_WINDOW: SessionIndex = 6;
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
keystore: Arc<LocalKeystore>,
|
||||||
|
highest_session: Option<SessionIndex>,
|
||||||
|
rolling_session_window: RollingSessionWindow,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for the dispute coordinator subsystem.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct Config {
|
||||||
|
/// The data column in the store to use for dispute data.
|
||||||
|
pub col_data: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
fn column_config(&self) -> db::v1::ColumnConfiguration {
|
||||||
|
db::v1::ColumnConfiguration { col_data: self.col_data }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An implementation of the dispute coordinator subsystem.
|
||||||
|
pub struct DisputeCoordinatorSubsystem {
|
||||||
|
config: Config,
|
||||||
|
store: Arc<dyn KeyValueDB>,
|
||||||
|
keystore: Arc<LocalKeystore>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisputeCoordinatorSubsystem {
|
||||||
|
/// Create a new instance of the subsystem.
|
||||||
|
pub fn new(
|
||||||
|
store: Arc<dyn KeyValueDB>,
|
||||||
|
config: Config,
|
||||||
|
keystore: Arc<LocalKeystore>,
|
||||||
|
) -> Self {
|
||||||
|
DisputeCoordinatorSubsystem { store, config, keystore }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Context> Subsystem<Context> for DisputeCoordinatorSubsystem
|
||||||
|
where Context: SubsystemContext<Message = DisputeCoordinatorMessage>
|
||||||
|
{
|
||||||
|
fn start(self, ctx: Context) -> SpawnedSubsystem {
|
||||||
|
let future = run(self, ctx)
|
||||||
|
.map(|_| Ok(()))
|
||||||
|
.boxed();
|
||||||
|
|
||||||
|
SpawnedSubsystem {
|
||||||
|
name: "dispute-coordinator-subsystem",
|
||||||
|
future,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
RuntimeApi(#[from] RuntimeApiError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
ChainApi(#[from] ChainApiError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Oneshot(#[from] oneshot::Canceled),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Subsystem(#[from] SubsystemError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Codec(#[from] CodecError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<db::v1::Error> for Error {
|
||||||
|
fn from(err: db::v1::Error) -> Self {
|
||||||
|
match err {
|
||||||
|
db::v1::Error::Io(io) => Self::Io(io),
|
||||||
|
db::v1::Error::Codec(e) => Self::Codec(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
fn trace(&self) {
|
||||||
|
match self {
|
||||||
|
// don't spam the log with spurious errors
|
||||||
|
Self::RuntimeApi(_) |
|
||||||
|
Self::Oneshot(_) => tracing::debug!(target: LOG_TARGET, err = ?self),
|
||||||
|
// it's worth reporting otherwise
|
||||||
|
_ => tracing::warn!(target: LOG_TARGET, err = ?self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run<Context>(subsystem: DisputeCoordinatorSubsystem, mut ctx: Context)
|
||||||
|
where Context: SubsystemContext<Message = DisputeCoordinatorMessage>
|
||||||
|
{
|
||||||
|
loop {
|
||||||
|
let res = run_iteration(&mut ctx, &subsystem).await;
|
||||||
|
match res {
|
||||||
|
Err(e) => {
|
||||||
|
e.trace();
|
||||||
|
|
||||||
|
if let Error::Subsystem(SubsystemError::Context(_)) = e {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(()) => {
|
||||||
|
tracing::info!(target: LOG_TARGET, "received `Conclude` signal, exiting");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the subsystem until an error is encountered or a `conclude` signal is received.
|
||||||
|
// Most errors are non-fatal and should lead to another call to this function.
|
||||||
|
//
|
||||||
|
// A return value of `Ok` indicates that an exit should be made, while non-fatal errors
|
||||||
|
// lead to another call to this function.
|
||||||
|
async fn run_iteration<Context>(ctx: &mut Context, subsystem: &DisputeCoordinatorSubsystem)
|
||||||
|
-> Result<(), Error>
|
||||||
|
where Context: SubsystemContext<Message = DisputeCoordinatorMessage>
|
||||||
|
{
|
||||||
|
let DisputeCoordinatorSubsystem { ref store, ref keystore, ref config } = *subsystem;
|
||||||
|
let mut state = State {
|
||||||
|
keystore: keystore.clone(),
|
||||||
|
highest_session: None,
|
||||||
|
rolling_session_window: RollingSessionWindow::new(DISPUTE_WINDOW),
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match ctx.recv().await? {
|
||||||
|
FromOverseer::Signal(OverseerSignal::Conclude) => {
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
FromOverseer::Signal(OverseerSignal::ActiveLeaves(update)) => {
|
||||||
|
handle_new_activations(
|
||||||
|
ctx,
|
||||||
|
&**store,
|
||||||
|
&mut state,
|
||||||
|
config,
|
||||||
|
update.activated.into_iter().map(|a| a.hash),
|
||||||
|
).await?
|
||||||
|
}
|
||||||
|
FromOverseer::Signal(OverseerSignal::BlockFinalized(_, _)) => {},
|
||||||
|
FromOverseer::Communication { msg } => {
|
||||||
|
handle_incoming(
|
||||||
|
ctx,
|
||||||
|
&**store,
|
||||||
|
&mut state,
|
||||||
|
config,
|
||||||
|
msg,
|
||||||
|
).await?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_new_activations(
|
||||||
|
ctx: &mut impl SubsystemContext,
|
||||||
|
store: &dyn KeyValueDB,
|
||||||
|
state: &mut State,
|
||||||
|
config: &Config,
|
||||||
|
new_activations: impl IntoIterator<Item = Hash>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
for new_leaf in new_activations {
|
||||||
|
let block_header = {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
|
ctx.send_message(
|
||||||
|
ChainApiMessage::BlockHeader(new_leaf, tx).into()
|
||||||
|
).await;
|
||||||
|
|
||||||
|
match rx.await?? {
|
||||||
|
None => continue,
|
||||||
|
Some(header) => header,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match state.rolling_session_window.cache_session_info_for_head(
|
||||||
|
ctx,
|
||||||
|
new_leaf,
|
||||||
|
&block_header,
|
||||||
|
).await {
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: LOG_TARGET,
|
||||||
|
err = ?e,
|
||||||
|
"Failed to update session cache for disputes",
|
||||||
|
);
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Ok(SessionWindowUpdate::Initialized { window_end, .. })
|
||||||
|
| Ok(SessionWindowUpdate::Advanced { new_window_end: window_end, .. })
|
||||||
|
=> {
|
||||||
|
let session = window_end;
|
||||||
|
if state.highest_session.map_or(true, |s| s < session) {
|
||||||
|
tracing::trace!(
|
||||||
|
target: LOG_TARGET,
|
||||||
|
session,
|
||||||
|
"Observed new session. Pruning",
|
||||||
|
);
|
||||||
|
|
||||||
|
state.highest_session = Some(session);
|
||||||
|
|
||||||
|
db::v1::note_current_session(
|
||||||
|
store,
|
||||||
|
&config.column_config(),
|
||||||
|
session,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO [after https://github.com/paritytech/polkadot/issues/3160]: chain rollbacks
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_incoming(
|
||||||
|
ctx: &mut impl SubsystemContext,
|
||||||
|
store: &dyn KeyValueDB,
|
||||||
|
state: &mut State,
|
||||||
|
config: &Config,
|
||||||
|
message: DisputeCoordinatorMessage,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
match message {
|
||||||
|
DisputeCoordinatorMessage::ImportStatements {
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt,
|
||||||
|
session,
|
||||||
|
statements,
|
||||||
|
} => {
|
||||||
|
handle_import_statements(
|
||||||
|
ctx,
|
||||||
|
store,
|
||||||
|
state,
|
||||||
|
config,
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt,
|
||||||
|
session,
|
||||||
|
statements,
|
||||||
|
).await?;
|
||||||
|
}
|
||||||
|
DisputeCoordinatorMessage::ActiveDisputes(rx) => {
|
||||||
|
let active_disputes = db::v1::load_active_disputes(store, &config.column_config())?
|
||||||
|
.map(|d| d.disputed)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let _ = rx.send(active_disputes);
|
||||||
|
}
|
||||||
|
DisputeCoordinatorMessage::QueryCandidateVotes(
|
||||||
|
session,
|
||||||
|
candidate_hash,
|
||||||
|
rx
|
||||||
|
) => {
|
||||||
|
let candidate_votes = db::v1::load_candidate_votes(
|
||||||
|
store,
|
||||||
|
&config.column_config(),
|
||||||
|
session,
|
||||||
|
&candidate_hash,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let _ = rx.send(candidate_votes.map(Into::into));
|
||||||
|
}
|
||||||
|
DisputeCoordinatorMessage::IssueLocalStatement(
|
||||||
|
session,
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt,
|
||||||
|
valid,
|
||||||
|
) => {
|
||||||
|
issue_local_statement(
|
||||||
|
ctx,
|
||||||
|
state,
|
||||||
|
store,
|
||||||
|
config,
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt,
|
||||||
|
session,
|
||||||
|
valid,
|
||||||
|
).await?;
|
||||||
|
}
|
||||||
|
DisputeCoordinatorMessage::DetermineUndisputedChain {
|
||||||
|
base_number,
|
||||||
|
block_descriptions,
|
||||||
|
tx,
|
||||||
|
} => {
|
||||||
|
let undisputed_chain = determine_undisputed_chain(
|
||||||
|
store,
|
||||||
|
&config,
|
||||||
|
base_number,
|
||||||
|
block_descriptions
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let _ = tx.send(undisputed_chain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_into_statement_vec<T>(
|
||||||
|
vec: &mut Vec<(T, ValidatorIndex, ValidatorSignature)>,
|
||||||
|
tag: T,
|
||||||
|
val_index: ValidatorIndex,
|
||||||
|
val_signature: ValidatorSignature,
|
||||||
|
) {
|
||||||
|
let pos = match vec.binary_search_by_key(&val_index, |x| x.1) {
|
||||||
|
Ok(_) => return, // no duplicates needed.
|
||||||
|
Err(p) => p,
|
||||||
|
};
|
||||||
|
|
||||||
|
vec.insert(pos, (tag, val_index, val_signature));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_import_statements(
|
||||||
|
ctx: &mut impl SubsystemContext,
|
||||||
|
store: &dyn KeyValueDB,
|
||||||
|
state: &mut State,
|
||||||
|
config: &Config,
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
candidate_receipt: CandidateReceipt,
|
||||||
|
session: SessionIndex,
|
||||||
|
statements: Vec<(SignedDisputeStatement, ValidatorIndex)>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
if state.highest_session.map_or(true, |h| session + DISPUTE_WINDOW < h) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let validators = match state.rolling_session_window.session_info(session) {
|
||||||
|
None => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: LOG_TARGET,
|
||||||
|
session,
|
||||||
|
"Missing info for session which has an active dispute",
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
Some(info) => info.validators.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let n_validators = validators.len();
|
||||||
|
|
||||||
|
let supermajority_threshold = polkadot_primitives::v1::supermajority_threshold(n_validators);
|
||||||
|
|
||||||
|
let mut votes = db::v1::load_candidate_votes(
|
||||||
|
store,
|
||||||
|
&config.column_config(),
|
||||||
|
session,
|
||||||
|
&candidate_hash
|
||||||
|
)?
|
||||||
|
.map(CandidateVotes::from)
|
||||||
|
.unwrap_or_else(|| CandidateVotes {
|
||||||
|
candidate_receipt: candidate_receipt.clone(),
|
||||||
|
valid: Vec::new(),
|
||||||
|
invalid: Vec::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let was_undisputed = votes.valid.is_empty() || votes.invalid.is_empty();
|
||||||
|
|
||||||
|
// Update candidate votes.
|
||||||
|
for (statement, val_index) in statements {
|
||||||
|
if validators.get(val_index.0 as usize)
|
||||||
|
.map_or(true, |v| v != statement.validator_public())
|
||||||
|
{
|
||||||
|
tracing::debug!(
|
||||||
|
target: LOG_TARGET,
|
||||||
|
?val_index,
|
||||||
|
session,
|
||||||
|
claimed_key = ?statement.validator_public(),
|
||||||
|
"Validator index doesn't match claimed key",
|
||||||
|
);
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
match statement.statement().clone() {
|
||||||
|
DisputeStatement::Valid(valid_kind) => {
|
||||||
|
insert_into_statement_vec(
|
||||||
|
&mut votes.valid,
|
||||||
|
valid_kind,
|
||||||
|
val_index,
|
||||||
|
statement.validator_signature().clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
DisputeStatement::Invalid(invalid_kind) => {
|
||||||
|
insert_into_statement_vec(
|
||||||
|
&mut votes.invalid,
|
||||||
|
invalid_kind,
|
||||||
|
val_index,
|
||||||
|
statement.validator_signature().clone(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if newly disputed.
|
||||||
|
let is_disputed = !votes.valid.is_empty() && !votes.invalid.is_empty();
|
||||||
|
let freshly_disputed = is_disputed && was_undisputed;
|
||||||
|
let already_disputed = is_disputed && !was_undisputed;
|
||||||
|
let concluded_valid = votes.valid.len() >= supermajority_threshold;
|
||||||
|
|
||||||
|
let mut tx = db::v1::Transaction::default();
|
||||||
|
|
||||||
|
if freshly_disputed && !concluded_valid {
|
||||||
|
// add to active disputes and begin local participation.
|
||||||
|
update_active_disputes(
|
||||||
|
store,
|
||||||
|
config,
|
||||||
|
&mut tx,
|
||||||
|
|active| active.insert(session, candidate_hash),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let voted_indices = votes.voted_indices();
|
||||||
|
|
||||||
|
ctx.send_message(DisputeParticipationMessage::Participate {
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt,
|
||||||
|
session,
|
||||||
|
voted_indices,
|
||||||
|
}.into()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if concluded_valid && already_disputed {
|
||||||
|
// remove from active disputes.
|
||||||
|
update_active_disputes(
|
||||||
|
store,
|
||||||
|
config,
|
||||||
|
&mut tx,
|
||||||
|
|active| active.delete(session, candidate_hash),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.put_candidate_votes(session, candidate_hash, votes.into());
|
||||||
|
tx.write(store, &config.column_config())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_active_disputes(
|
||||||
|
store: &dyn KeyValueDB,
|
||||||
|
config: &Config,
|
||||||
|
tx: &mut db::v1::Transaction,
|
||||||
|
with_active: impl FnOnce(&mut ActiveDisputes) -> bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let mut active_disputes = db::v1::load_active_disputes(store, &config.column_config())?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if with_active(&mut active_disputes) {
|
||||||
|
tx.put_active_disputes(active_disputes);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn issue_local_statement(
|
||||||
|
ctx: &mut impl SubsystemContext,
|
||||||
|
state: &mut State,
|
||||||
|
store: &dyn KeyValueDB,
|
||||||
|
config: &Config,
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
candidate_receipt: CandidateReceipt,
|
||||||
|
session: SessionIndex,
|
||||||
|
valid: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
// Load session info.
|
||||||
|
let validators = match state.rolling_session_window.session_info(session) {
|
||||||
|
None => {
|
||||||
|
tracing::warn!(
|
||||||
|
target: LOG_TARGET,
|
||||||
|
session,
|
||||||
|
"Missing info for session which has an active dispute",
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(())
|
||||||
|
}
|
||||||
|
Some(info) => info.validators.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let votes = db::v1::load_candidate_votes(
|
||||||
|
store,
|
||||||
|
&config.column_config(),
|
||||||
|
session,
|
||||||
|
&candidate_hash
|
||||||
|
)?
|
||||||
|
.map(CandidateVotes::from)
|
||||||
|
.unwrap_or_else(|| CandidateVotes {
|
||||||
|
candidate_receipt: candidate_receipt.clone(),
|
||||||
|
valid: Vec::new(),
|
||||||
|
invalid: Vec::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sign a statement for each validator index we control which has
|
||||||
|
// not already voted. This should generally be maximum 1 statement.
|
||||||
|
let voted_indices = votes.voted_indices();
|
||||||
|
let mut statements = Vec::new();
|
||||||
|
|
||||||
|
let voted_indices: HashSet<_> = voted_indices.into_iter().collect();
|
||||||
|
for (index, validator) in validators.iter().enumerate() {
|
||||||
|
let index = ValidatorIndex(index as _);
|
||||||
|
if voted_indices.contains(&index) { continue }
|
||||||
|
if state.keystore.key_pair::<ValidatorPair>(validator).ok().flatten().is_none() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let keystore = state.keystore.clone() as Arc<_>;
|
||||||
|
let res = SignedDisputeStatement::sign_explicit(
|
||||||
|
&keystore,
|
||||||
|
valid,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
validator.clone(),
|
||||||
|
).await;
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(Some(signed_dispute_statement)) => {
|
||||||
|
statements.push((signed_dispute_statement, index));
|
||||||
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
target: LOG_TARGET,
|
||||||
|
err = ?e,
|
||||||
|
"Encountered keystore error while signing dispute statement",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do import
|
||||||
|
if !statements.is_empty() {
|
||||||
|
handle_import_statements(
|
||||||
|
ctx,
|
||||||
|
store,
|
||||||
|
state,
|
||||||
|
config,
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt,
|
||||||
|
session,
|
||||||
|
statements,
|
||||||
|
).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn determine_undisputed_chain(
|
||||||
|
store: &dyn KeyValueDB,
|
||||||
|
config: &Config,
|
||||||
|
base_number: BlockNumber,
|
||||||
|
block_descriptions: Vec<(Hash, SessionIndex, Vec<CandidateHash>)>,
|
||||||
|
) -> Result<Option<(BlockNumber, Hash)>, Error> {
|
||||||
|
let last = block_descriptions.last()
|
||||||
|
.map(|e| (base_number + block_descriptions.len() as BlockNumber, e.0));
|
||||||
|
|
||||||
|
// Fast path for no disputes.
|
||||||
|
let active_disputes = match db::v1::load_active_disputes(store, &config.column_config())? {
|
||||||
|
None => return Ok(last),
|
||||||
|
Some(a) if a.disputed.is_empty() => return Ok(last),
|
||||||
|
Some(a) => a,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (i, (_, session, candidates)) in block_descriptions.iter().enumerate() {
|
||||||
|
if candidates.iter().any(|c| active_disputes.contains(*session, *c)) {
|
||||||
|
if i == 0 {
|
||||||
|
return Ok(None);
|
||||||
|
} else {
|
||||||
|
return Ok(Some((
|
||||||
|
base_number + i as BlockNumber,
|
||||||
|
block_descriptions[i - 1].0,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(last)
|
||||||
|
}
|
||||||
@@ -0,0 +1,706 @@
|
|||||||
|
// Copyright 2021 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Polkadot.
|
||||||
|
|
||||||
|
// Polkadot is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Polkadot is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use polkadot_primitives::v1::{BlakeTwo256, HashT, ValidatorId, Header, SessionInfo};
|
||||||
|
use polkadot_node_subsystem::{jaeger, ActiveLeavesUpdate, ActivatedLeaf, LeafStatus};
|
||||||
|
use polkadot_node_subsystem::messages::{
|
||||||
|
AllMessages, ChainApiMessage, RuntimeApiMessage, RuntimeApiRequest,
|
||||||
|
};
|
||||||
|
use polkadot_node_subsystem_test_helpers::{make_subsystem_context, TestSubsystemContextHandle};
|
||||||
|
use sp_core::testing::TaskExecutor;
|
||||||
|
use sp_keyring::Sr25519Keyring;
|
||||||
|
use sp_keystore::{SyncCryptoStore, SyncCryptoStorePtr};
|
||||||
|
use futures::future::{self, BoxFuture};
|
||||||
|
use parity_scale_codec::Encode;
|
||||||
|
use assert_matches::assert_matches;
|
||||||
|
|
||||||
|
// sets up a keystore with the given keyring accounts.
|
||||||
|
fn make_keystore(accounts: &[Sr25519Keyring]) -> LocalKeystore {
|
||||||
|
let store = LocalKeystore::in_memory();
|
||||||
|
|
||||||
|
for s in accounts.iter().copied().map(|k| k.to_seed()) {
|
||||||
|
store.sr25519_generate_new(
|
||||||
|
polkadot_primitives::v1::PARACHAIN_KEY_TYPE_ID,
|
||||||
|
Some(s.as_str()),
|
||||||
|
).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
store
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_to_hash(session: SessionIndex, extra: impl Encode) -> Hash {
|
||||||
|
BlakeTwo256::hash_of(&(session, extra))
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualOverseer = TestSubsystemContextHandle<DisputeCoordinatorMessage>;
|
||||||
|
|
||||||
|
struct TestState {
|
||||||
|
validators: Vec<Sr25519Keyring>,
|
||||||
|
validator_public: Vec<ValidatorId>,
|
||||||
|
validator_groups: Vec<Vec<ValidatorIndex>>,
|
||||||
|
master_keystore: Arc<sc_keystore::LocalKeystore>,
|
||||||
|
subsystem_keystore: Arc<sc_keystore::LocalKeystore>,
|
||||||
|
db: Arc<dyn KeyValueDB>,
|
||||||
|
config: Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TestState {
|
||||||
|
fn default() -> TestState {
|
||||||
|
let validators = vec![
|
||||||
|
Sr25519Keyring::Alice,
|
||||||
|
Sr25519Keyring::Bob,
|
||||||
|
Sr25519Keyring::Charlie,
|
||||||
|
Sr25519Keyring::Dave,
|
||||||
|
Sr25519Keyring::Eve,
|
||||||
|
Sr25519Keyring::One,
|
||||||
|
];
|
||||||
|
|
||||||
|
let validator_public = validators.iter()
|
||||||
|
.map(|k| ValidatorId::from(k.public()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let validator_groups = vec![
|
||||||
|
vec![ValidatorIndex(0), ValidatorIndex(1)],
|
||||||
|
vec![ValidatorIndex(2), ValidatorIndex(3)],
|
||||||
|
vec![ValidatorIndex(4), ValidatorIndex(5)],
|
||||||
|
];
|
||||||
|
|
||||||
|
let master_keystore = make_keystore(&validators).into();
|
||||||
|
let subsystem_keystore = make_keystore(&[Sr25519Keyring::Alice]).into();
|
||||||
|
|
||||||
|
let db = Arc::new(kvdb_memorydb::create(1));
|
||||||
|
let config = Config {
|
||||||
|
col_data: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
TestState {
|
||||||
|
validators,
|
||||||
|
validator_public,
|
||||||
|
validator_groups,
|
||||||
|
master_keystore,
|
||||||
|
subsystem_keystore,
|
||||||
|
db,
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestState {
|
||||||
|
async fn activate_leaf_at_session(
|
||||||
|
&self,
|
||||||
|
virtual_overseer: &mut VirtualOverseer,
|
||||||
|
session: SessionIndex,
|
||||||
|
block_number: BlockNumber,
|
||||||
|
) {
|
||||||
|
assert!(block_number > 0);
|
||||||
|
|
||||||
|
let parent_hash = session_to_hash(session, b"parent");
|
||||||
|
let block_header = Header {
|
||||||
|
parent_hash,
|
||||||
|
number: block_number,
|
||||||
|
digest: Default::default(),
|
||||||
|
state_root: Default::default(),
|
||||||
|
extrinsics_root: Default::default(),
|
||||||
|
};
|
||||||
|
let block_hash = block_header.hash();
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Signal(OverseerSignal::ActiveLeaves(
|
||||||
|
ActiveLeavesUpdate::start_work(ActivatedLeaf {
|
||||||
|
hash: block_hash,
|
||||||
|
span: Arc::new(jaeger::Span::Disabled),
|
||||||
|
number: block_number,
|
||||||
|
status: LeafStatus::Fresh,
|
||||||
|
})
|
||||||
|
))).await;
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
virtual_overseer.recv().await,
|
||||||
|
AllMessages::ChainApi(ChainApiMessage::BlockHeader(h, tx)) => {
|
||||||
|
assert_eq!(h, block_hash);
|
||||||
|
let _ = tx.send(Ok(Some(block_header)));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
virtual_overseer.recv().await,
|
||||||
|
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||||
|
h,
|
||||||
|
RuntimeApiRequest::SessionIndexForChild(tx),
|
||||||
|
)) => {
|
||||||
|
assert_eq!(h, parent_hash);
|
||||||
|
let _ = tx.send(Ok(session));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// answer session info queries until the current session is reached.
|
||||||
|
assert_matches!(
|
||||||
|
virtual_overseer.recv().await,
|
||||||
|
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||||
|
h,
|
||||||
|
RuntimeApiRequest::SessionInfo(session_index, tx),
|
||||||
|
)) => {
|
||||||
|
assert_eq!(h, block_hash);
|
||||||
|
|
||||||
|
let _ = tx.send(Ok(Some(self.session_info())));
|
||||||
|
if session_index == session { break }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session_info(&self) -> SessionInfo {
|
||||||
|
let discovery_keys = self.validators.iter()
|
||||||
|
.map(|k| <_>::from(k.public()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let assignment_keys = self.validators.iter()
|
||||||
|
.map(|k| <_>::from(k.public()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
SessionInfo {
|
||||||
|
validators: self.validator_public.clone(),
|
||||||
|
discovery_keys,
|
||||||
|
assignment_keys,
|
||||||
|
validator_groups: self.validator_groups.clone(),
|
||||||
|
n_cores: self.validator_groups.len() as _,
|
||||||
|
zeroth_delay_tranche_width: 0,
|
||||||
|
relay_vrf_modulo_samples: 1,
|
||||||
|
n_delay_tranches: 100,
|
||||||
|
no_show_slots: 1,
|
||||||
|
needed_approvals: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn issue_statement_with_index(
|
||||||
|
&self,
|
||||||
|
index: usize,
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
session: SessionIndex,
|
||||||
|
valid: bool,
|
||||||
|
) -> SignedDisputeStatement {
|
||||||
|
let public = self.validator_public[index].clone();
|
||||||
|
|
||||||
|
let keystore = self.master_keystore.clone() as SyncCryptoStorePtr;
|
||||||
|
|
||||||
|
SignedDisputeStatement::sign_explicit(
|
||||||
|
&keystore,
|
||||||
|
valid,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
public,
|
||||||
|
).await.unwrap().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_harness<F>(test: F)
|
||||||
|
where F: FnOnce(TestState, VirtualOverseer) -> BoxFuture<'static, ()>
|
||||||
|
{
|
||||||
|
let (ctx, ctx_handle) = make_subsystem_context(TaskExecutor::new());
|
||||||
|
|
||||||
|
let state = TestState::default();
|
||||||
|
let subsystem = DisputeCoordinatorSubsystem::new(
|
||||||
|
state.db.clone(),
|
||||||
|
state.config.clone(),
|
||||||
|
state.subsystem_keystore.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let subsystem_task = run(subsystem, ctx);
|
||||||
|
let test_task = test(state, ctx_handle);
|
||||||
|
|
||||||
|
futures::executor::block_on(future::join(subsystem_task, test_task));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conflicting_votes_lead_to_dispute_participation() {
|
||||||
|
test_harness(|test_state, mut virtual_overseer| Box::pin(async move {
|
||||||
|
let session = 1;
|
||||||
|
|
||||||
|
let candidate_receipt = CandidateReceipt::default();
|
||||||
|
let candidate_hash = candidate_receipt.hash();
|
||||||
|
|
||||||
|
test_state.activate_leaf_at_session(
|
||||||
|
&mut virtual_overseer,
|
||||||
|
session,
|
||||||
|
1,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let valid_vote = test_state.issue_statement_with_index(
|
||||||
|
0,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
true,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let invalid_vote = test_state.issue_statement_with_index(
|
||||||
|
1,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
false,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let invalid_vote_2 = test_state.issue_statement_with_index(
|
||||||
|
2,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
false,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ImportStatements {
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt: candidate_receipt.clone(),
|
||||||
|
session,
|
||||||
|
statements: vec![
|
||||||
|
(valid_vote, ValidatorIndex(0)),
|
||||||
|
(invalid_vote, ValidatorIndex(1)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
virtual_overseer.recv().await,
|
||||||
|
AllMessages::DisputeParticipation(DisputeParticipationMessage::Participate {
|
||||||
|
candidate_hash: c_hash,
|
||||||
|
candidate_receipt: c_receipt,
|
||||||
|
session: s,
|
||||||
|
voted_indices,
|
||||||
|
}) => {
|
||||||
|
assert_eq!(c_hash, candidate_hash);
|
||||||
|
assert_eq!(c_receipt, candidate_receipt);
|
||||||
|
assert_eq!(s, session);
|
||||||
|
assert_eq!(voted_indices, vec![ValidatorIndex(0), ValidatorIndex(1)]);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ActiveDisputes(tx),
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
assert_eq!(rx.await.unwrap(), vec![(session, candidate_hash)]);
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::QueryCandidateVotes(
|
||||||
|
session,
|
||||||
|
candidate_hash,
|
||||||
|
tx,
|
||||||
|
),
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
let votes = rx.await.unwrap().unwrap();
|
||||||
|
assert_eq!(votes.valid.len(), 1);
|
||||||
|
assert_eq!(votes.invalid.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ImportStatements {
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt: candidate_receipt.clone(),
|
||||||
|
session,
|
||||||
|
statements: vec![
|
||||||
|
(invalid_vote_2, ValidatorIndex(2)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
{
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::QueryCandidateVotes(
|
||||||
|
session,
|
||||||
|
candidate_hash,
|
||||||
|
tx,
|
||||||
|
),
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
let votes = rx.await.unwrap().unwrap();
|
||||||
|
assert_eq!(votes.valid.len(), 1);
|
||||||
|
assert_eq!(votes.invalid.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Signal(OverseerSignal::Conclude)).await;
|
||||||
|
|
||||||
|
// This confirms that the second vote doesn't lead to participation again.
|
||||||
|
assert!(virtual_overseer.try_recv().await.is_none());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn positive_votes_dont_trigger_participation() {
|
||||||
|
test_harness(|test_state, mut virtual_overseer| Box::pin(async move {
|
||||||
|
let session = 1;
|
||||||
|
|
||||||
|
let candidate_receipt = CandidateReceipt::default();
|
||||||
|
let candidate_hash = candidate_receipt.hash();
|
||||||
|
|
||||||
|
test_state.activate_leaf_at_session(
|
||||||
|
&mut virtual_overseer,
|
||||||
|
session,
|
||||||
|
1,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let valid_vote = test_state.issue_statement_with_index(
|
||||||
|
0,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
true,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let valid_vote_2 = test_state.issue_statement_with_index(
|
||||||
|
1,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
true,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ImportStatements {
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt: candidate_receipt.clone(),
|
||||||
|
session,
|
||||||
|
statements: vec![
|
||||||
|
(valid_vote, ValidatorIndex(0)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
{
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ActiveDisputes(tx),
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
assert!(rx.await.unwrap().is_empty());
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::QueryCandidateVotes(
|
||||||
|
session,
|
||||||
|
candidate_hash,
|
||||||
|
tx,
|
||||||
|
),
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
let votes = rx.await.unwrap().unwrap();
|
||||||
|
assert_eq!(votes.valid.len(), 1);
|
||||||
|
assert!(votes.invalid.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ImportStatements {
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt: candidate_receipt.clone(),
|
||||||
|
session,
|
||||||
|
statements: vec![
|
||||||
|
(valid_vote_2, ValidatorIndex(1)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
{
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ActiveDisputes(tx),
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
assert!(rx.await.unwrap().is_empty());
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::QueryCandidateVotes(
|
||||||
|
session,
|
||||||
|
candidate_hash,
|
||||||
|
tx,
|
||||||
|
),
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
let votes = rx.await.unwrap().unwrap();
|
||||||
|
assert_eq!(votes.valid.len(), 2);
|
||||||
|
assert!(votes.invalid.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Signal(OverseerSignal::Conclude)).await;
|
||||||
|
|
||||||
|
// This confirms that no participation request is made.
|
||||||
|
assert!(virtual_overseer.try_recv().await.is_none());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrong_validator_index_is_ignored() {
|
||||||
|
test_harness(|test_state, mut virtual_overseer| Box::pin(async move {
|
||||||
|
let session = 1;
|
||||||
|
|
||||||
|
let candidate_receipt = CandidateReceipt::default();
|
||||||
|
let candidate_hash = candidate_receipt.hash();
|
||||||
|
|
||||||
|
test_state.activate_leaf_at_session(
|
||||||
|
&mut virtual_overseer,
|
||||||
|
session,
|
||||||
|
1,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let valid_vote = test_state.issue_statement_with_index(
|
||||||
|
0,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
true,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let invalid_vote = test_state.issue_statement_with_index(
|
||||||
|
1,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
false,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ImportStatements {
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt: candidate_receipt.clone(),
|
||||||
|
session,
|
||||||
|
statements: vec![
|
||||||
|
(valid_vote, ValidatorIndex(1)),
|
||||||
|
(invalid_vote, ValidatorIndex(0)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
{
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ActiveDisputes(tx),
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
assert!(rx.await.unwrap().is_empty());
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::QueryCandidateVotes(
|
||||||
|
session,
|
||||||
|
candidate_hash,
|
||||||
|
tx,
|
||||||
|
),
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
let votes = rx.await.unwrap().unwrap();
|
||||||
|
assert!(votes.valid.is_empty());
|
||||||
|
assert!(votes.invalid.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Signal(OverseerSignal::Conclude)).await;
|
||||||
|
|
||||||
|
// This confirms that no participation request is made.
|
||||||
|
assert!(virtual_overseer.try_recv().await.is_none());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finality_votes_ignore_disputed_candidates() {
|
||||||
|
test_harness(|test_state, mut virtual_overseer| Box::pin(async move {
|
||||||
|
let session = 1;
|
||||||
|
|
||||||
|
let candidate_receipt = CandidateReceipt::default();
|
||||||
|
let candidate_hash = candidate_receipt.hash();
|
||||||
|
|
||||||
|
test_state.activate_leaf_at_session(
|
||||||
|
&mut virtual_overseer,
|
||||||
|
session,
|
||||||
|
1,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let valid_vote = test_state.issue_statement_with_index(
|
||||||
|
0,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
true,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let invalid_vote = test_state.issue_statement_with_index(
|
||||||
|
1,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
false,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ImportStatements {
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt: candidate_receipt.clone(),
|
||||||
|
session,
|
||||||
|
statements: vec![
|
||||||
|
(valid_vote, ValidatorIndex(0)),
|
||||||
|
(invalid_vote, ValidatorIndex(1)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).await;
|
||||||
|
let _ = virtual_overseer.recv().await;
|
||||||
|
|
||||||
|
{
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
|
let block_hash_a = Hash::repeat_byte(0x0a);
|
||||||
|
let block_hash_b = Hash::repeat_byte(0x0b);
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::DetermineUndisputedChain {
|
||||||
|
base_number: 10,
|
||||||
|
block_descriptions: vec![
|
||||||
|
(block_hash_a, session, vec![candidate_hash]),
|
||||||
|
],
|
||||||
|
tx,
|
||||||
|
},
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
assert!(rx.await.unwrap().is_none());
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::DetermineUndisputedChain {
|
||||||
|
base_number: 10,
|
||||||
|
block_descriptions: vec![
|
||||||
|
(block_hash_a, session, vec![]),
|
||||||
|
(block_hash_b, session, vec![candidate_hash]),
|
||||||
|
],
|
||||||
|
tx,
|
||||||
|
},
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
assert_eq!(rx.await.unwrap(), Some((11, block_hash_a)));
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Signal(OverseerSignal::Conclude)).await;
|
||||||
|
assert!(virtual_overseer.try_recv().await.is_none());
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn supermajority_valid_dispute_may_be_finalized() {
|
||||||
|
test_harness(|test_state, mut virtual_overseer| Box::pin(async move {
|
||||||
|
let session = 1;
|
||||||
|
|
||||||
|
let candidate_receipt = CandidateReceipt::default();
|
||||||
|
let candidate_hash = candidate_receipt.hash();
|
||||||
|
|
||||||
|
test_state.activate_leaf_at_session(
|
||||||
|
&mut virtual_overseer,
|
||||||
|
session,
|
||||||
|
1,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let supermajority_threshold = polkadot_primitives::v1::supermajority_threshold(
|
||||||
|
test_state.validators.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
let valid_vote = test_state.issue_statement_with_index(
|
||||||
|
0,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
true,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
let invalid_vote = test_state.issue_statement_with_index(
|
||||||
|
1,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
false,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ImportStatements {
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt: candidate_receipt.clone(),
|
||||||
|
session,
|
||||||
|
statements: vec![
|
||||||
|
(valid_vote, ValidatorIndex(0)),
|
||||||
|
(invalid_vote, ValidatorIndex(1)),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
let _ = virtual_overseer.recv().await;
|
||||||
|
|
||||||
|
let mut statements = Vec::new();
|
||||||
|
for i in (0..supermajority_threshold - 1).map(|i| i + 2) {
|
||||||
|
let vote = test_state.issue_statement_with_index(
|
||||||
|
i,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
true,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
statements.push((vote, ValidatorIndex(i as _)));
|
||||||
|
};
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ImportStatements {
|
||||||
|
candidate_hash,
|
||||||
|
candidate_receipt: candidate_receipt.clone(),
|
||||||
|
session,
|
||||||
|
statements,
|
||||||
|
},
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
{
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::ActiveDisputes(tx),
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
assert!(rx.await.unwrap().is_empty());
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
|
let block_hash_a = Hash::repeat_byte(0x0a);
|
||||||
|
let block_hash_b = Hash::repeat_byte(0x0b);
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::DetermineUndisputedChain {
|
||||||
|
base_number: 10,
|
||||||
|
block_descriptions: vec![
|
||||||
|
(block_hash_a, session, vec![candidate_hash]),
|
||||||
|
],
|
||||||
|
tx,
|
||||||
|
},
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
assert_eq!(rx.await.unwrap(), Some((11, block_hash_a)));
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
virtual_overseer.send(FromOverseer::Communication {
|
||||||
|
msg: DisputeCoordinatorMessage::DetermineUndisputedChain {
|
||||||
|
base_number: 10,
|
||||||
|
block_descriptions: vec![
|
||||||
|
(block_hash_a, session, vec![]),
|
||||||
|
(block_hash_b, session, vec![candidate_hash]),
|
||||||
|
],
|
||||||
|
tx,
|
||||||
|
},
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
assert_eq!(rx.await.unwrap(), Some((12, block_hash_b)));
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual_overseer.send(FromOverseer::Signal(OverseerSignal::Conclude)).await;
|
||||||
|
assert!(virtual_overseer.try_recv().await.is_none());
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -1250,11 +1250,13 @@ fn spread_event_to_subsystems_is_up_to_date() {
|
|||||||
AllMessages::ApprovalVoting(_) => unreachable!("Not interested in network events"),
|
AllMessages::ApprovalVoting(_) => unreachable!("Not interested in network events"),
|
||||||
AllMessages::ApprovalDistribution(_) => { cnt += 1; }
|
AllMessages::ApprovalDistribution(_) => { cnt += 1; }
|
||||||
AllMessages::GossipSupport(_) => unreachable!("Not interested in network events"),
|
AllMessages::GossipSupport(_) => unreachable!("Not interested in network events"),
|
||||||
// Add variants here as needed, `{ cnt += 1; }` for those that need to be
|
AllMessages::DisputeCoordinator(_) => unreachable!("Not interested in network events"),
|
||||||
// notified, `unreachable!()` for those that should not.
|
AllMessages::DisputeParticipation(_) => unreachable!("Not interetsed in network events"),
|
||||||
}
|
// Add variants here as needed, `{ cnt += 1; }` for those that need to be
|
||||||
}
|
// notified, `unreachable!()` for those that should not.
|
||||||
assert_eq!(cnt, EXPECTED_COUNT);
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(cnt, EXPECTED_COUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -629,6 +629,8 @@ impl ChannelsOut {
|
|||||||
AllMessages::GossipSupport(msg) => {
|
AllMessages::GossipSupport(msg) => {
|
||||||
self.gossip_support.send(make_packet(signals_received, msg)).await
|
self.gossip_support.send(make_packet(signals_received, msg)).await
|
||||||
},
|
},
|
||||||
|
AllMessages::DisputeCoordinator(_) => Ok(()),
|
||||||
|
AllMessages::DisputeParticipation(_) => Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if res.is_err() {
|
if res.is_err() {
|
||||||
@@ -731,6 +733,8 @@ impl ChannelsOut {
|
|||||||
.unbounded_send(make_packet(signals_received, msg))
|
.unbounded_send(make_packet(signals_received, msg))
|
||||||
.map_err(|e| e.into_send_error())
|
.map_err(|e| e.into_send_error())
|
||||||
},
|
},
|
||||||
|
AllMessages::DisputeCoordinator(_) => Ok(()),
|
||||||
|
AllMessages::DisputeParticipation(_) => Ok(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if res.is_err() {
|
if res.is_err() {
|
||||||
@@ -2062,6 +2066,8 @@ where
|
|||||||
AllMessages::GossipSupport(msg) => {
|
AllMessages::GossipSupport(msg) => {
|
||||||
self.subsystems.gossip_support.send_message(msg).await?;
|
self.subsystems.gossip_support.send_message(msg).await?;
|
||||||
},
|
},
|
||||||
|
AllMessages::DisputeCoordinator(_) => {}
|
||||||
|
AllMessages::DisputeParticipation(_) => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
|||||||
sp-application-crypto = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
sp-application-crypto = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||||
sp-consensus-vrf = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
sp-consensus-vrf = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||||
sp-consensus-babe = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
sp-consensus-babe = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||||
|
sp-keystore = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||||
sp-maybe-compressed-blob = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
sp-maybe-compressed-blob = { git = "https://github.com/paritytech/substrate", branch = "master" }
|
||||||
polkadot-parachain = { path = "../../parachain", default-features = false }
|
polkadot-parachain = { path = "../../parachain", default-features = false }
|
||||||
schnorrkel = "0.9.1"
|
schnorrkel = "0.9.1"
|
||||||
|
|||||||
@@ -98,10 +98,6 @@ pub struct IndirectAssignmentCert {
|
|||||||
pub cert: AssignmentCert,
|
pub cert: AssignmentCert,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A vote of approval on a candidate.
|
|
||||||
#[derive(Debug, Clone, Encode, Decode)]
|
|
||||||
pub struct ApprovalVote(pub CandidateHash);
|
|
||||||
|
|
||||||
/// A signed approval vote which references the candidate indirectly via the block.
|
/// A signed approval vote which references the candidate indirectly via the block.
|
||||||
///
|
///
|
||||||
/// In practice, we have a look-up from block hash and candidate index to candidate hash,
|
/// In practice, we have a look-up from block hash and candidate index to candidate hash,
|
||||||
|
|||||||
@@ -22,18 +22,28 @@
|
|||||||
|
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
|
|
||||||
|
use std::convert::TryInto;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use parity_scale_codec::{Decode, Encode};
|
use parity_scale_codec::{Decode, Encode};
|
||||||
|
use sp_keystore::{CryptoStore, SyncCryptoStorePtr, Error as KeystoreError};
|
||||||
|
use sp_application_crypto::AppKey;
|
||||||
|
|
||||||
pub use sp_core::traits::SpawnNamed;
|
pub use sp_core::traits::SpawnNamed;
|
||||||
pub use sp_consensus_babe::{
|
pub use sp_consensus_babe::{
|
||||||
Epoch as BabeEpoch, BabeEpochConfiguration, AllowedSlots as BabeAllowedSlots,
|
Epoch as BabeEpoch, BabeEpochConfiguration, AllowedSlots as BabeAllowedSlots,
|
||||||
};
|
};
|
||||||
|
|
||||||
use polkadot_primitives::v1::{BlakeTwo256, CandidateCommitments, CandidateHash, CollatorPair, CommittedCandidateReceipt, CompactStatement, EncodeAs, Hash, HashT, HeadData, Id as ParaId, OutboundHrmpMessage, PersistedValidationData, Signed, UncheckedSigned, UpwardMessage, ValidationCode, ValidatorIndex};
|
use polkadot_primitives::v1::{
|
||||||
|
BlakeTwo256, CandidateCommitments, CandidateHash, CollatorPair, CommittedCandidateReceipt,
|
||||||
|
CompactStatement, EncodeAs, Hash, HashT, HeadData, Id as ParaId, OutboundHrmpMessage,
|
||||||
|
PersistedValidationData, Signed, UncheckedSigned, UpwardMessage, ValidationCode,
|
||||||
|
ValidatorIndex, ValidatorSignature, ValidDisputeStatementKind, InvalidDisputeStatementKind,
|
||||||
|
CandidateReceipt, ValidatorId, SessionIndex, DisputeStatement,
|
||||||
|
};
|
||||||
|
|
||||||
pub use polkadot_parachain::primitives::BlockData;
|
pub use polkadot_parachain::primitives::BlockData;
|
||||||
|
|
||||||
pub mod approval;
|
pub mod approval;
|
||||||
@@ -273,3 +283,125 @@ pub fn maybe_compress_pov(pov: PoV) -> PoV {
|
|||||||
let pov = PoV { block_data: BlockData(raw) };
|
let pov = PoV { block_data: BlockData(raw) };
|
||||||
pov
|
pov
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tracked votes on candidates, for the purposes of dispute resolution.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CandidateVotes {
|
||||||
|
/// The receipt of the candidate itself.
|
||||||
|
pub candidate_receipt: CandidateReceipt,
|
||||||
|
/// Votes of validity, sorted by validator index.
|
||||||
|
pub valid: Vec<(ValidDisputeStatementKind, ValidatorIndex, ValidatorSignature)>,
|
||||||
|
/// Votes of invalidity, sorted by validator index.
|
||||||
|
pub invalid: Vec<(InvalidDisputeStatementKind, ValidatorIndex, ValidatorSignature)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CandidateVotes {
|
||||||
|
/// Get the set of all validators who have votes in the set, ascending.
|
||||||
|
pub fn voted_indices(&self) -> Vec<ValidatorIndex> {
|
||||||
|
let mut v: Vec<_> = self.valid.iter().map(|x| x.1).chain(
|
||||||
|
self.invalid.iter().map(|x| x.1)
|
||||||
|
).collect();
|
||||||
|
|
||||||
|
v.sort();
|
||||||
|
v.dedup();
|
||||||
|
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// A checked dispute statement from an associated validator.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SignedDisputeStatement {
|
||||||
|
dispute_statement: DisputeStatement,
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
validator_public: ValidatorId,
|
||||||
|
validator_signature: ValidatorSignature,
|
||||||
|
session_index: SessionIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignedDisputeStatement {
|
||||||
|
/// Create a new `SignedDisputeStatement`, which is only possible by checking the signature.
|
||||||
|
pub fn new_checked(
|
||||||
|
dispute_statement: DisputeStatement,
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
session_index: SessionIndex,
|
||||||
|
validator_public: ValidatorId,
|
||||||
|
validator_signature: ValidatorSignature,
|
||||||
|
) -> Result<Self, ()> {
|
||||||
|
dispute_statement.check_signature(
|
||||||
|
&validator_public,
|
||||||
|
candidate_hash,
|
||||||
|
session_index,
|
||||||
|
&validator_signature,
|
||||||
|
).map(|_| SignedDisputeStatement {
|
||||||
|
dispute_statement,
|
||||||
|
candidate_hash,
|
||||||
|
validator_public,
|
||||||
|
validator_signature,
|
||||||
|
session_index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign this statement with the given keystore and key. Pass `valid = true` to
|
||||||
|
/// indicate validity of the candidate, and `valid = false` to indicate invalidity.
|
||||||
|
pub async fn sign_explicit(
|
||||||
|
keystore: &SyncCryptoStorePtr,
|
||||||
|
valid: bool,
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
session_index: SessionIndex,
|
||||||
|
validator_public: ValidatorId,
|
||||||
|
) -> Result<Option<Self>, KeystoreError> {
|
||||||
|
let dispute_statement = if valid {
|
||||||
|
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit)
|
||||||
|
} else {
|
||||||
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit)
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = dispute_statement.payload_data(candidate_hash, session_index);
|
||||||
|
let signature = CryptoStore::sign_with(
|
||||||
|
&**keystore,
|
||||||
|
ValidatorId::ID,
|
||||||
|
&validator_public.clone().into(),
|
||||||
|
&data,
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
let signature = match signature {
|
||||||
|
Some(sig) => sig.try_into().map_err(|_| KeystoreError::KeyNotSupported(ValidatorId::ID))?,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(Self {
|
||||||
|
dispute_statement,
|
||||||
|
candidate_hash,
|
||||||
|
validator_public,
|
||||||
|
validator_signature: signature,
|
||||||
|
session_index,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the underlying dispute statement
|
||||||
|
pub fn statement(&self) -> &DisputeStatement {
|
||||||
|
&self.dispute_statement
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the underlying candidate hash.
|
||||||
|
pub fn candidate_hash(&self) -> &CandidateHash {
|
||||||
|
&self.candidate_hash
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the underlying validator public key.
|
||||||
|
pub fn validator_public(&self) -> &ValidatorId {
|
||||||
|
&self.validator_public
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the underlying validator signature.
|
||||||
|
pub fn validator_signature(&self) -> &ValidatorSignature {
|
||||||
|
&self.validator_signature
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the underlying session index.
|
||||||
|
pub fn session_index(&self) -> SessionIndex {
|
||||||
|
self.session_index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ pub mod reexports {
|
|||||||
|
|
||||||
/// Convenient and efficient runtime info access.
|
/// Convenient and efficient runtime info access.
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
|
/// A rolling session window cache.
|
||||||
|
pub mod rolling_session_window;
|
||||||
|
|
||||||
mod error_handling;
|
mod error_handling;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,635 @@
|
|||||||
|
// Copyright 2021 Parity Technologies (UK) Ltd.
|
||||||
|
// This file is part of Polkadot.
|
||||||
|
|
||||||
|
// Polkadot is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Polkadot is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
//! A rolling window of sessions and cached session info, updated by the state of newly imported blocks.
|
||||||
|
//!
|
||||||
|
//! This is useful for consensus components which need to stay up-to-date about recent sessions but don't
|
||||||
|
//! care about the state of particular blocks.
|
||||||
|
|
||||||
|
use polkadot_primitives::v1::{Hash, Header, SessionInfo, SessionIndex};
|
||||||
|
use polkadot_node_subsystem::{
|
||||||
|
SubsystemContext,
|
||||||
|
messages::{RuntimeApiMessage, RuntimeApiRequest},
|
||||||
|
errors::RuntimeApiError,
|
||||||
|
};
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
|
||||||
|
/// Sessions unavailable in state to cache.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SessionsUnavailableKind {
|
||||||
|
/// Runtime API subsystem was unavailable.
|
||||||
|
RuntimeApiUnavailable(oneshot::Canceled),
|
||||||
|
/// The runtime API itself returned an error.
|
||||||
|
RuntimeApi(RuntimeApiError),
|
||||||
|
/// Missing session info from runtime API.
|
||||||
|
Missing,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about the sessions being fetched.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SessionsUnavailableInfo {
|
||||||
|
/// The desired window start.
|
||||||
|
pub window_start: SessionIndex,
|
||||||
|
/// The desired window end.
|
||||||
|
pub window_end: SessionIndex,
|
||||||
|
/// The block hash whose state the sessions were meant to be drawn from.
|
||||||
|
pub block_hash: Hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sessions were unavailable to fetch from the state for some reason.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SessionsUnavailable {
|
||||||
|
/// The error kind.
|
||||||
|
kind: SessionsUnavailableKind,
|
||||||
|
/// The info about the session window, if any.
|
||||||
|
info: Option<SessionsUnavailableInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An indicated update of the rolling session window.
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub enum SessionWindowUpdate {
|
||||||
|
/// The session window was just initialized to the current values.
|
||||||
|
Initialized {
|
||||||
|
/// The start of the window (inclusive).
|
||||||
|
window_start: SessionIndex,
|
||||||
|
/// The end of the window (inclusive).
|
||||||
|
window_end: SessionIndex,
|
||||||
|
},
|
||||||
|
/// The session window was just advanced from one range to a new one.
|
||||||
|
Advanced {
|
||||||
|
/// The previous start of the window (inclusive).
|
||||||
|
prev_window_start: SessionIndex,
|
||||||
|
/// The previous end of the window (inclusive).
|
||||||
|
prev_window_end: SessionIndex,
|
||||||
|
/// The new start of the window (inclusive).
|
||||||
|
new_window_start: SessionIndex,
|
||||||
|
/// The new end of the window (inclusive).
|
||||||
|
new_window_end: SessionIndex,
|
||||||
|
},
|
||||||
|
/// The session window was unchanged.
|
||||||
|
Unchanged,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rolling window of sessions and cached session info.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct RollingSessionWindow {
|
||||||
|
earliest_session: Option<SessionIndex>,
|
||||||
|
session_info: Vec<SessionInfo>,
|
||||||
|
window_size: SessionIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RollingSessionWindow {
|
||||||
|
/// Initialize a new session info cache with the given window size.
|
||||||
|
pub fn new(window_size: SessionIndex) -> Self {
|
||||||
|
RollingSessionWindow {
|
||||||
|
earliest_session: None,
|
||||||
|
session_info: Vec::new(),
|
||||||
|
window_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize a new session info cache with the given window size and
|
||||||
|
/// initial data.
|
||||||
|
pub fn with_session_info(
|
||||||
|
window_size: SessionIndex,
|
||||||
|
earliest_session: SessionIndex,
|
||||||
|
session_info: Vec<SessionInfo>,
|
||||||
|
) -> Self {
|
||||||
|
RollingSessionWindow {
|
||||||
|
earliest_session: Some(earliest_session),
|
||||||
|
session_info,
|
||||||
|
window_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the session info for the given session index, if stored within the window.
|
||||||
|
pub fn session_info(&self, index: SessionIndex) -> Option<&SessionInfo> {
|
||||||
|
self.earliest_session.and_then(|earliest| {
|
||||||
|
if index < earliest {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
self.session_info.get((index - earliest) as usize)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the index of the earliest session, if the window is not empty.
|
||||||
|
pub fn earliest_session(&self) -> Option<SessionIndex> {
|
||||||
|
self.earliest_session.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Access the index of the latest session, if the window is not empty.
|
||||||
|
pub fn latest_session(&self) -> Option<SessionIndex> {
|
||||||
|
self.earliest_session
|
||||||
|
.map(|earliest| earliest + (self.session_info.len() as SessionIndex).saturating_sub(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When inspecting a new import notification, updates the session info cache to match
|
||||||
|
/// the session of the imported block.
|
||||||
|
///
|
||||||
|
/// this only needs to be called on heads where we are directly notified about import, as sessions do
|
||||||
|
/// not change often and import notifications are expected to be typically increasing in session number.
|
||||||
|
///
|
||||||
|
/// some backwards drift in session index is acceptable.
|
||||||
|
pub async fn cache_session_info_for_head(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut impl SubsystemContext,
|
||||||
|
block_hash: Hash,
|
||||||
|
block_header: &Header,
|
||||||
|
) -> Result<SessionWindowUpdate, SessionsUnavailable> {
|
||||||
|
if self.window_size == 0 { return Ok(SessionWindowUpdate::Unchanged) }
|
||||||
|
|
||||||
|
let session_index = {
|
||||||
|
let (s_tx, s_rx) = oneshot::channel();
|
||||||
|
|
||||||
|
// The genesis is guaranteed to be at the beginning of the session and its parent state
|
||||||
|
// is non-existent. Therefore if we're at the genesis, we request using its state and
|
||||||
|
// not the parent.
|
||||||
|
ctx.send_message(RuntimeApiMessage::Request(
|
||||||
|
if block_header.number == 0 { block_hash } else { block_header.parent_hash },
|
||||||
|
RuntimeApiRequest::SessionIndexForChild(s_tx),
|
||||||
|
).into()).await;
|
||||||
|
|
||||||
|
match s_rx.await {
|
||||||
|
Ok(Ok(s)) => s,
|
||||||
|
Ok(Err(e)) => return Err(SessionsUnavailable {
|
||||||
|
kind: SessionsUnavailableKind::RuntimeApi(e),
|
||||||
|
info: None,
|
||||||
|
}),
|
||||||
|
Err(e) => return Err(SessionsUnavailable {
|
||||||
|
kind: SessionsUnavailableKind::RuntimeApiUnavailable(e),
|
||||||
|
info: None,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.earliest_session {
|
||||||
|
None => {
|
||||||
|
// First block processed on start-up.
|
||||||
|
|
||||||
|
let window_start = session_index.saturating_sub(self.window_size - 1);
|
||||||
|
|
||||||
|
match load_all_sessions(ctx, block_hash, window_start, session_index).await {
|
||||||
|
Err(kind) => {
|
||||||
|
Err(SessionsUnavailable {
|
||||||
|
kind,
|
||||||
|
info: Some(SessionsUnavailableInfo {
|
||||||
|
window_start,
|
||||||
|
window_end: session_index,
|
||||||
|
block_hash,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
Ok(s) => {
|
||||||
|
let update = SessionWindowUpdate::Initialized {
|
||||||
|
window_start,
|
||||||
|
window_end: session_index,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.earliest_session = Some(window_start);
|
||||||
|
self.session_info = s;
|
||||||
|
|
||||||
|
Ok(update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(old_window_start) => {
|
||||||
|
let latest = self.latest_session().expect("latest always exists if earliest does; qed");
|
||||||
|
|
||||||
|
// Either cached or ancient.
|
||||||
|
if session_index <= latest { return Ok(SessionWindowUpdate::Unchanged) }
|
||||||
|
|
||||||
|
let old_window_end = latest;
|
||||||
|
|
||||||
|
let window_start = session_index.saturating_sub(self.window_size - 1);
|
||||||
|
|
||||||
|
// keep some of the old window, if applicable.
|
||||||
|
let overlap_start = window_start.saturating_sub(old_window_start);
|
||||||
|
|
||||||
|
let fresh_start = if latest < window_start {
|
||||||
|
window_start
|
||||||
|
} else {
|
||||||
|
latest + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
match load_all_sessions(ctx, block_hash, fresh_start, session_index).await {
|
||||||
|
Err(kind) => {
|
||||||
|
Err(SessionsUnavailable {
|
||||||
|
kind,
|
||||||
|
info: Some(SessionsUnavailableInfo {
|
||||||
|
window_start: latest +1,
|
||||||
|
window_end: session_index,
|
||||||
|
block_hash,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
Ok(s) => {
|
||||||
|
let update = SessionWindowUpdate::Advanced {
|
||||||
|
prev_window_start: old_window_start,
|
||||||
|
prev_window_end: old_window_end,
|
||||||
|
new_window_start: window_start,
|
||||||
|
new_window_end: session_index,
|
||||||
|
};
|
||||||
|
|
||||||
|
let outdated = std::cmp::min(overlap_start as usize, self.session_info.len());
|
||||||
|
self.session_info.drain(..outdated);
|
||||||
|
self.session_info.extend(s);
|
||||||
|
// we need to account for this case:
|
||||||
|
// window_start ................................... session_index
|
||||||
|
// old_window_start ........... latest
|
||||||
|
let new_earliest = std::cmp::max(window_start, old_window_start);
|
||||||
|
self.earliest_session = Some(new_earliest);
|
||||||
|
|
||||||
|
Ok(update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_all_sessions(
|
||||||
|
ctx: &mut impl SubsystemContext,
|
||||||
|
block_hash: Hash,
|
||||||
|
start: SessionIndex,
|
||||||
|
end_inclusive: SessionIndex,
|
||||||
|
) -> Result<Vec<SessionInfo>, SessionsUnavailableKind> {
|
||||||
|
let mut v = Vec::new();
|
||||||
|
for i in start..=end_inclusive {
|
||||||
|
let (tx, rx)= oneshot::channel();
|
||||||
|
ctx.send_message(RuntimeApiMessage::Request(
|
||||||
|
block_hash,
|
||||||
|
RuntimeApiRequest::SessionInfo(i, tx),
|
||||||
|
).into()).await;
|
||||||
|
|
||||||
|
let session_info = match rx.await {
|
||||||
|
Ok(Ok(Some(s))) => s,
|
||||||
|
Ok(Ok(None)) => {
|
||||||
|
return Err(SessionsUnavailableKind::Missing);
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => return Err(SessionsUnavailableKind::RuntimeApi(e)),
|
||||||
|
Err(canceled) => return Err(SessionsUnavailableKind::RuntimeApiUnavailable(canceled)),
|
||||||
|
};
|
||||||
|
|
||||||
|
v.push(session_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use polkadot_node_subsystem_test_helpers::make_subsystem_context;
|
||||||
|
use polkadot_node_subsystem::messages::AllMessages;
|
||||||
|
use sp_core::testing::TaskExecutor;
|
||||||
|
use assert_matches::assert_matches;
|
||||||
|
|
||||||
|
const TEST_WINDOW_SIZE: SessionIndex = 6;
|
||||||
|
|
||||||
|
fn dummy_session_info(index: SessionIndex) -> SessionInfo {
|
||||||
|
SessionInfo {
|
||||||
|
validators: Vec::new(),
|
||||||
|
discovery_keys: Vec::new(),
|
||||||
|
assignment_keys: Vec::new(),
|
||||||
|
validator_groups: Vec::new(),
|
||||||
|
n_cores: index as _,
|
||||||
|
zeroth_delay_tranche_width: index as _,
|
||||||
|
relay_vrf_modulo_samples: index as _,
|
||||||
|
n_delay_tranches: index as _,
|
||||||
|
no_show_slots: index as _,
|
||||||
|
needed_approvals: index as _,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cache_session_info_test(
|
||||||
|
expected_start_session: SessionIndex,
|
||||||
|
session: SessionIndex,
|
||||||
|
mut window: RollingSessionWindow,
|
||||||
|
expect_requests_from: SessionIndex,
|
||||||
|
) {
|
||||||
|
let header = Header {
|
||||||
|
digest: Default::default(),
|
||||||
|
extrinsics_root: Default::default(),
|
||||||
|
number: 5,
|
||||||
|
state_root: Default::default(),
|
||||||
|
parent_hash: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let pool = TaskExecutor::new();
|
||||||
|
let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone());
|
||||||
|
|
||||||
|
let hash = header.hash();
|
||||||
|
|
||||||
|
let test_fut = {
|
||||||
|
let header = header.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
window.cache_session_info_for_head(
|
||||||
|
&mut ctx,
|
||||||
|
hash,
|
||||||
|
&header,
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(window.earliest_session, Some(expected_start_session));
|
||||||
|
assert_eq!(
|
||||||
|
window.session_info,
|
||||||
|
(expected_start_session..=session).map(dummy_session_info).collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let aux_fut = Box::pin(async move {
|
||||||
|
assert_matches!(
|
||||||
|
handle.recv().await,
|
||||||
|
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||||
|
h,
|
||||||
|
RuntimeApiRequest::SessionIndexForChild(s_tx),
|
||||||
|
)) => {
|
||||||
|
assert_eq!(h, header.parent_hash);
|
||||||
|
let _ = s_tx.send(Ok(session));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
for i in expect_requests_from..=session {
|
||||||
|
assert_matches!(
|
||||||
|
handle.recv().await,
|
||||||
|
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||||
|
h,
|
||||||
|
RuntimeApiRequest::SessionInfo(j, s_tx),
|
||||||
|
)) => {
|
||||||
|
assert_eq!(h, hash);
|
||||||
|
assert_eq!(i, j);
|
||||||
|
let _ = s_tx.send(Ok(Some(dummy_session_info(i))));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
futures::executor::block_on(futures::future::join(test_fut, aux_fut));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_session_info_first_early() {
|
||||||
|
cache_session_info_test(
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
RollingSessionWindow::new(TEST_WINDOW_SIZE),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_session_info_does_not_underflow() {
|
||||||
|
let window = RollingSessionWindow {
|
||||||
|
earliest_session: Some(1),
|
||||||
|
session_info: vec![dummy_session_info(1)],
|
||||||
|
window_size: TEST_WINDOW_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
cache_session_info_test(
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
window,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_session_info_first_late() {
|
||||||
|
cache_session_info_test(
|
||||||
|
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE - 1),
|
||||||
|
100,
|
||||||
|
RollingSessionWindow::new(TEST_WINDOW_SIZE),
|
||||||
|
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE - 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_session_info_jump() {
|
||||||
|
let window = RollingSessionWindow {
|
||||||
|
earliest_session: Some(50),
|
||||||
|
session_info: vec![dummy_session_info(50), dummy_session_info(51), dummy_session_info(52)],
|
||||||
|
window_size: TEST_WINDOW_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
cache_session_info_test(
|
||||||
|
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE - 1),
|
||||||
|
100,
|
||||||
|
window,
|
||||||
|
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE - 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_session_info_roll_full() {
|
||||||
|
let start = 99 - (TEST_WINDOW_SIZE - 1);
|
||||||
|
let window = RollingSessionWindow {
|
||||||
|
earliest_session: Some(start),
|
||||||
|
session_info: (start..=99).map(dummy_session_info).collect(),
|
||||||
|
window_size: TEST_WINDOW_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
cache_session_info_test(
|
||||||
|
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE - 1),
|
||||||
|
100,
|
||||||
|
window,
|
||||||
|
100, // should only make one request.
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_session_info_roll_many_full() {
|
||||||
|
let start = 97 - (TEST_WINDOW_SIZE - 1);
|
||||||
|
let window = RollingSessionWindow {
|
||||||
|
earliest_session: Some(start),
|
||||||
|
session_info: (start..=97).map(dummy_session_info).collect(),
|
||||||
|
window_size: TEST_WINDOW_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
cache_session_info_test(
|
||||||
|
(100 as SessionIndex).saturating_sub(TEST_WINDOW_SIZE - 1),
|
||||||
|
100,
|
||||||
|
window,
|
||||||
|
98,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_session_info_roll_early() {
|
||||||
|
let start = 0;
|
||||||
|
let window = RollingSessionWindow {
|
||||||
|
earliest_session: Some(start),
|
||||||
|
session_info: (0..=1).map(dummy_session_info).collect(),
|
||||||
|
window_size: TEST_WINDOW_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
cache_session_info_test(
|
||||||
|
0,
|
||||||
|
2,
|
||||||
|
window,
|
||||||
|
2, // should only make one request.
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_session_info_roll_many_early() {
|
||||||
|
let start = 0;
|
||||||
|
let window = RollingSessionWindow {
|
||||||
|
earliest_session: Some(start),
|
||||||
|
session_info: (0..=1).map(dummy_session_info).collect(),
|
||||||
|
window_size: TEST_WINDOW_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
cache_session_info_test(
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
window,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn any_session_unavailable_for_caching_means_no_change() {
|
||||||
|
let session: SessionIndex = 6;
|
||||||
|
let start_session = session.saturating_sub(TEST_WINDOW_SIZE - 1);
|
||||||
|
|
||||||
|
let header = Header {
|
||||||
|
digest: Default::default(),
|
||||||
|
extrinsics_root: Default::default(),
|
||||||
|
number: 5,
|
||||||
|
state_root: Default::default(),
|
||||||
|
parent_hash: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let pool = TaskExecutor::new();
|
||||||
|
let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone());
|
||||||
|
|
||||||
|
let mut window = RollingSessionWindow::new(TEST_WINDOW_SIZE);
|
||||||
|
let hash = header.hash();
|
||||||
|
|
||||||
|
let test_fut = {
|
||||||
|
let header = header.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
let res = window.cache_session_info_for_head(
|
||||||
|
&mut ctx,
|
||||||
|
hash,
|
||||||
|
&header,
|
||||||
|
).await;
|
||||||
|
|
||||||
|
assert!(res.is_err());
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let aux_fut = Box::pin(async move {
|
||||||
|
assert_matches!(
|
||||||
|
handle.recv().await,
|
||||||
|
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||||
|
h,
|
||||||
|
RuntimeApiRequest::SessionIndexForChild(s_tx),
|
||||||
|
)) => {
|
||||||
|
assert_eq!(h, header.parent_hash);
|
||||||
|
let _ = s_tx.send(Ok(session));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
for i in start_session..=session {
|
||||||
|
assert_matches!(
|
||||||
|
handle.recv().await,
|
||||||
|
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||||
|
h,
|
||||||
|
RuntimeApiRequest::SessionInfo(j, s_tx),
|
||||||
|
)) => {
|
||||||
|
assert_eq!(h, hash);
|
||||||
|
assert_eq!(i, j);
|
||||||
|
|
||||||
|
let _ = s_tx.send(Ok(if i == session {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(dummy_session_info(i))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
futures::executor::block_on(futures::future::join(test_fut, aux_fut));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_session_info_for_genesis() {
|
||||||
|
let session: SessionIndex = 0;
|
||||||
|
|
||||||
|
let header = Header {
|
||||||
|
digest: Default::default(),
|
||||||
|
extrinsics_root: Default::default(),
|
||||||
|
number: 0,
|
||||||
|
state_root: Default::default(),
|
||||||
|
parent_hash: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let pool = TaskExecutor::new();
|
||||||
|
let (mut ctx, mut handle) = make_subsystem_context::<(), _>(pool.clone());
|
||||||
|
|
||||||
|
let mut window = RollingSessionWindow::new(TEST_WINDOW_SIZE);
|
||||||
|
let hash = header.hash();
|
||||||
|
|
||||||
|
let test_fut = {
|
||||||
|
let header = header.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
window.cache_session_info_for_head(
|
||||||
|
&mut ctx,
|
||||||
|
hash,
|
||||||
|
&header,
|
||||||
|
).await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(window.earliest_session, Some(session));
|
||||||
|
assert_eq!(
|
||||||
|
window.session_info,
|
||||||
|
vec![dummy_session_info(session)],
|
||||||
|
);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
let aux_fut = Box::pin(async move {
|
||||||
|
assert_matches!(
|
||||||
|
handle.recv().await,
|
||||||
|
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||||
|
h,
|
||||||
|
RuntimeApiRequest::SessionIndexForChild(s_tx),
|
||||||
|
)) => {
|
||||||
|
assert_eq!(h, hash);
|
||||||
|
let _ = s_tx.send(Ok(session));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
handle.recv().await,
|
||||||
|
AllMessages::RuntimeApi(RuntimeApiMessage::Request(
|
||||||
|
h,
|
||||||
|
RuntimeApiRequest::SessionInfo(s, s_tx),
|
||||||
|
)) => {
|
||||||
|
assert_eq!(h, hash);
|
||||||
|
assert_eq!(s, session);
|
||||||
|
|
||||||
|
let _ = s_tx.send(Ok(Some(dummy_session_info(s))));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
futures::executor::block_on(futures::future::join(test_fut, aux_fut));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,8 +36,8 @@ use polkadot_node_network_protocol::{
|
|||||||
};
|
};
|
||||||
use polkadot_node_primitives::{
|
use polkadot_node_primitives::{
|
||||||
approval::{BlockApprovalMeta, IndirectAssignmentCert, IndirectSignedApprovalVote},
|
approval::{BlockApprovalMeta, IndirectAssignmentCert, IndirectSignedApprovalVote},
|
||||||
AvailableData, BabeEpoch, CollationGenerationConfig, ErasureChunk, PoV, SignedFullStatement,
|
AvailableData, BabeEpoch, CandidateVotes, CollationGenerationConfig, ErasureChunk, PoV,
|
||||||
ValidationResult,
|
SignedDisputeStatement, SignedFullStatement, ValidationResult,
|
||||||
};
|
};
|
||||||
use polkadot_primitives::v1::{
|
use polkadot_primitives::v1::{
|
||||||
AuthorityDiscoveryId, BackedCandidate, BlockNumber, CandidateDescriptor, CandidateEvent,
|
AuthorityDiscoveryId, BackedCandidate, BlockNumber, CandidateDescriptor, CandidateEvent,
|
||||||
@@ -188,6 +188,73 @@ impl BoundToRelayParent for CollatorProtocolMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Messages received by the dispute coordinator subsystem.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DisputeCoordinatorMessage {
|
||||||
|
/// Import a statement by a validator about a candidate.
|
||||||
|
///
|
||||||
|
/// The subsystem will silently discard ancient statements or sets of only dispute-specific statements for
|
||||||
|
/// candidates that are previously unknown to the subsystem. The former is simply because ancient
|
||||||
|
/// data is not relevant and the latter is as a DoS prevention mechanism. Both backing and approval
|
||||||
|
/// statements already undergo anti-DoS procedures in their respective subsystems, but statements
|
||||||
|
/// cast specifically for disputes are not necessarily relevant to any candidate the system is
|
||||||
|
/// already aware of and thus present a DoS vector. Our expectation is that nodes will notify each
|
||||||
|
/// other of disputes over the network by providing (at least) 2 conflicting statements, of which one is either
|
||||||
|
/// a backing or validation statement.
|
||||||
|
///
|
||||||
|
/// This does not do any checking of the message signature.
|
||||||
|
ImportStatements {
|
||||||
|
/// The hash of the candidate.
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
/// The candidate receipt itself.
|
||||||
|
candidate_receipt: CandidateReceipt,
|
||||||
|
/// The session the candidate appears in.
|
||||||
|
session: SessionIndex,
|
||||||
|
/// Statements, with signatures checked, by validators participating in disputes.
|
||||||
|
///
|
||||||
|
/// The validator index passed alongside each statement should correspond to the index
|
||||||
|
/// of the validator in the set.
|
||||||
|
statements: Vec<(SignedDisputeStatement, ValidatorIndex)>,
|
||||||
|
},
|
||||||
|
/// Fetch a list of all active disputes that the coordinator is aware of.
|
||||||
|
ActiveDisputes(oneshot::Sender<Vec<(SessionIndex, CandidateHash)>>),
|
||||||
|
/// Get candidate votes for a candidate.
|
||||||
|
QueryCandidateVotes(SessionIndex, CandidateHash, oneshot::Sender<Option<CandidateVotes>>),
|
||||||
|
/// Sign and issue local dispute votes. A value of `true` indicates validity, and `false` invalidity.
|
||||||
|
IssueLocalStatement(SessionIndex, CandidateHash, CandidateReceipt, bool),
|
||||||
|
/// Determine the highest undisputed block within the given chain, based on where candidates
|
||||||
|
/// were included. If even the base block should not be finalized due to a dispute,
|
||||||
|
/// then `None` should be returned on the channel.
|
||||||
|
///
|
||||||
|
/// The block descriptions begin counting upwards from the block after the given `base_number`. The `base_number`
|
||||||
|
/// is typically the number of the last finalized block but may be slightly higher. This block
|
||||||
|
/// is inevitably going to be finalized so it is not accounted for by this function.
|
||||||
|
DetermineUndisputedChain {
|
||||||
|
/// The number of the lowest possible block to vote on.
|
||||||
|
base_number: BlockNumber,
|
||||||
|
/// Descriptions of all the blocks counting upwards from the block after the base number
|
||||||
|
block_descriptions: Vec<(Hash, SessionIndex, Vec<CandidateHash>)>,
|
||||||
|
/// A response channel - `None` to vote on base, `Some` to vote higher.
|
||||||
|
tx: oneshot::Sender<Option<(BlockNumber, Hash)>>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Messages received by the dispute participation subsystem.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum DisputeParticipationMessage {
|
||||||
|
/// Validate a candidate for the purposes of participating in a dispute.
|
||||||
|
Participate {
|
||||||
|
/// The hash of the candidate
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
/// The candidate receipt itself.
|
||||||
|
candidate_receipt: CandidateReceipt,
|
||||||
|
/// The session the candidate appears in.
|
||||||
|
session: SessionIndex,
|
||||||
|
/// The indices of validators who have already voted on this candidate.
|
||||||
|
voted_indices: Vec<ValidatorIndex>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Messages received by the network bridge subsystem.
|
/// Messages received by the network bridge subsystem.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum NetworkBridgeMessage {
|
pub enum NetworkBridgeMessage {
|
||||||
@@ -703,6 +770,12 @@ pub enum AllMessages {
|
|||||||
/// Message for the Gossip Support subsystem.
|
/// Message for the Gossip Support subsystem.
|
||||||
#[skip]
|
#[skip]
|
||||||
GossipSupport(GossipSupportMessage),
|
GossipSupport(GossipSupportMessage),
|
||||||
|
/// Message for the dispute coordinator subsystem.
|
||||||
|
#[skip]
|
||||||
|
DisputeCoordinator(DisputeCoordinatorMessage),
|
||||||
|
/// Message for the dispute participation subsystem.
|
||||||
|
#[skip]
|
||||||
|
DisputeParticipation(DisputeParticipationMessage),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<IncomingRequest<req_res_v1::PoVFetchingRequest>> for AvailabilityDistributionMessage {
|
impl From<IncomingRequest<req_res_v1::PoVFetchingRequest>> for AvailabilityDistributionMessage {
|
||||||
|
|||||||
@@ -672,6 +672,14 @@ pub enum CompactStatement {
|
|||||||
Valid(CandidateHash),
|
Valid(CandidateHash),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CompactStatement {
|
||||||
|
/// Yields the payload used for validator signatures on this kind
|
||||||
|
/// of statement.
|
||||||
|
pub fn signing_payload(&self, context: &SigningContext) -> Vec<u8> {
|
||||||
|
(self, context).encode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Inner helper for codec on `CompactStatement`.
|
// Inner helper for codec on `CompactStatement`.
|
||||||
#[derive(Encode, Decode)]
|
#[derive(Encode, Decode)]
|
||||||
enum CompactStatementInner {
|
enum CompactStatementInner {
|
||||||
|
|||||||
@@ -842,6 +842,22 @@ pub struct SessionInfo {
|
|||||||
pub needed_approvals: u32,
|
pub needed_approvals: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A vote of approval on a candidate.
|
||||||
|
#[derive(Clone, RuntimeDebug)]
|
||||||
|
pub struct ApprovalVote(pub CandidateHash);
|
||||||
|
|
||||||
|
impl ApprovalVote {
|
||||||
|
/// Yields the signing payload for this approval vote.
|
||||||
|
pub fn signing_payload(
|
||||||
|
&self,
|
||||||
|
session_index: SessionIndex,
|
||||||
|
) -> Vec<u8> {
|
||||||
|
const MAGIC: [u8; 4] = *b"APPR";
|
||||||
|
|
||||||
|
(MAGIC, &self.0, session_index).encode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sp_api::decl_runtime_apis! {
|
sp_api::decl_runtime_apis! {
|
||||||
/// The API for querying the state of parachains on-chain.
|
/// The API for querying the state of parachains on-chain.
|
||||||
pub trait ParachainHost<H: Decode = Hash, N: Encode + Decode = BlockNumber> {
|
pub trait ParachainHost<H: Decode = Hash, N: Encode + Decode = BlockNumber> {
|
||||||
@@ -1064,6 +1080,60 @@ pub enum DisputeStatement {
|
|||||||
Invalid(InvalidDisputeStatementKind),
|
Invalid(InvalidDisputeStatementKind),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DisputeStatement {
|
||||||
|
/// Get the payload data for this type of dispute statement.
|
||||||
|
pub fn payload_data(&self, candidate_hash: CandidateHash, session: SessionIndex) -> Vec<u8> {
|
||||||
|
match *self {
|
||||||
|
DisputeStatement::Valid(ValidDisputeStatementKind::Explicit) => {
|
||||||
|
ExplicitDisputeStatement {
|
||||||
|
valid: true,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
}.signing_payload()
|
||||||
|
},
|
||||||
|
DisputeStatement::Valid(ValidDisputeStatementKind::BackingSeconded(inclusion_parent)) => {
|
||||||
|
CompactStatement::Seconded(candidate_hash).signing_payload(&SigningContext {
|
||||||
|
session_index: session,
|
||||||
|
parent_hash: inclusion_parent,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
DisputeStatement::Valid(ValidDisputeStatementKind::BackingValid(inclusion_parent)) => {
|
||||||
|
CompactStatement::Valid(candidate_hash).signing_payload(&SigningContext {
|
||||||
|
session_index: session,
|
||||||
|
parent_hash: inclusion_parent,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
DisputeStatement::Valid(ValidDisputeStatementKind::ApprovalChecking) => {
|
||||||
|
ApprovalVote(candidate_hash).signing_payload(session)
|
||||||
|
},
|
||||||
|
DisputeStatement::Invalid(InvalidDisputeStatementKind::Explicit) => {
|
||||||
|
ExplicitDisputeStatement {
|
||||||
|
valid: false,
|
||||||
|
candidate_hash,
|
||||||
|
session,
|
||||||
|
}.signing_payload()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check the signature on a dispute statement.
|
||||||
|
pub fn check_signature(
|
||||||
|
&self,
|
||||||
|
validator_public: &ValidatorId,
|
||||||
|
candidate_hash: CandidateHash,
|
||||||
|
session: SessionIndex,
|
||||||
|
validator_signature: &ValidatorSignature,
|
||||||
|
) -> Result<(), ()> {
|
||||||
|
let payload = self.payload_data(candidate_hash, session);
|
||||||
|
|
||||||
|
if validator_signature.verify(&payload[..] , &validator_public) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Different kinds of statements of validity on a candidate.
|
/// Different kinds of statements of validity on a candidate.
|
||||||
#[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug)]
|
#[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug)]
|
||||||
pub enum ValidDisputeStatementKind {
|
pub enum ValidDisputeStatementKind {
|
||||||
@@ -1072,10 +1142,10 @@ pub enum ValidDisputeStatementKind {
|
|||||||
Explicit,
|
Explicit,
|
||||||
/// A seconded statement on a candidate from the backing phase.
|
/// A seconded statement on a candidate from the backing phase.
|
||||||
#[codec(index = 1)]
|
#[codec(index = 1)]
|
||||||
BackingSeconded,
|
BackingSeconded(Hash),
|
||||||
/// A valid statement on a candidate from the backing phase.
|
/// A valid statement on a candidate from the backing phase.
|
||||||
#[codec(index = 2)]
|
#[codec(index = 2)]
|
||||||
BackingValid,
|
BackingValid(Hash),
|
||||||
/// An approval vote from the approval checking phase.
|
/// An approval vote from the approval checking phase.
|
||||||
#[codec(index = 3)]
|
#[codec(index = 3)]
|
||||||
ApprovalChecking,
|
ApprovalChecking,
|
||||||
@@ -1090,7 +1160,7 @@ pub enum InvalidDisputeStatementKind {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An explicit statement on a candidate issued as part of a dispute.
|
/// An explicit statement on a candidate issued as part of a dispute.
|
||||||
#[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug)]
|
#[derive(Clone, PartialEq, RuntimeDebug)]
|
||||||
pub struct ExplicitDisputeStatement {
|
pub struct ExplicitDisputeStatement {
|
||||||
/// Whether the candidate is valid
|
/// Whether the candidate is valid
|
||||||
pub valid: bool,
|
pub valid: bool,
|
||||||
@@ -1100,6 +1170,15 @@ pub struct ExplicitDisputeStatement {
|
|||||||
pub session: SessionIndex,
|
pub session: SessionIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ExplicitDisputeStatement {
|
||||||
|
/// Produce the payload used for signing this type of statement.
|
||||||
|
pub fn signing_payload(&self) -> Vec<u8> {
|
||||||
|
const MAGIC: [u8; 4] = *b"DISP";
|
||||||
|
|
||||||
|
(MAGIC, self.valid, self.candidate_hash, self.session).encode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A set of statements about a specific candidate.
|
/// A set of statements about a specific candidate.
|
||||||
#[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug)]
|
#[derive(Encode, Decode, Clone, PartialEq, RuntimeDebug)]
|
||||||
pub struct DisputeStatementSet {
|
pub struct DisputeStatementSet {
|
||||||
@@ -1140,6 +1219,19 @@ pub struct InherentData<HDR: HeaderT = Header> {
|
|||||||
pub parent_header: HDR,
|
pub parent_header: HDR,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The maximum number of validators `f` which may safely be faulty.
|
||||||
|
///
|
||||||
|
/// The total number of validators is `n = 3f + e` where `e in { 1, 2, 3 }`.
|
||||||
|
pub fn byzantine_threshold(n: usize) -> usize {
|
||||||
|
n.saturating_sub(1) / 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The supermajority threshold of validators which represents a subset
|
||||||
|
/// guaranteed to have at least f+1 honest validators.
|
||||||
|
pub fn supermajority_threshold(n: usize) -> usize {
|
||||||
|
n - byzantine_threshold(n)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -1190,4 +1282,28 @@ mod tests {
|
|||||||
&Hash::repeat_byte(4).into(),
|
&Hash::repeat_byte(4).into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_byzantine_threshold() {
|
||||||
|
assert_eq!(byzantine_threshold(0), 0);
|
||||||
|
assert_eq!(byzantine_threshold(1), 0);
|
||||||
|
assert_eq!(byzantine_threshold(2), 0);
|
||||||
|
assert_eq!(byzantine_threshold(3), 0);
|
||||||
|
assert_eq!(byzantine_threshold(4), 1);
|
||||||
|
assert_eq!(byzantine_threshold(5), 1);
|
||||||
|
assert_eq!(byzantine_threshold(6), 1);
|
||||||
|
assert_eq!(byzantine_threshold(7), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_supermajority_threshold() {
|
||||||
|
assert_eq!(supermajority_threshold(0), 0);
|
||||||
|
assert_eq!(supermajority_threshold(1), 1);
|
||||||
|
assert_eq!(supermajority_threshold(2), 2);
|
||||||
|
assert_eq!(supermajority_threshold(3), 3);
|
||||||
|
assert_eq!(supermajority_threshold(4), 3);
|
||||||
|
assert_eq!(supermajority_threshold(5), 4);
|
||||||
|
assert_eq!(supermajority_threshold(6), 5);
|
||||||
|
assert_eq!(supermajority_threshold(7), 5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ We use an underlying Key-Value database where we assume we have the following op
|
|||||||
We use this database to encode the following schema:
|
We use this database to encode the following schema:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
(SessionIndex, "candidate-votes", CandidateHash) -> Option<CandidateVotes>
|
("candidate-votes", SessionIndex, CandidateHash) -> Option<CandidateVotes>
|
||||||
"active-disputes" -> ActiveDisputes
|
"active-disputes" -> ActiveDisputes
|
||||||
"earliest-session" -> Option<SessionIndex>
|
"earliest-session" -> Option<SessionIndex>
|
||||||
```
|
```
|
||||||
@@ -55,8 +55,6 @@ Ephemeral in-memory state:
|
|||||||
```rust
|
```rust
|
||||||
struct State {
|
struct State {
|
||||||
keystore: KeyStore,
|
keystore: KeyStore,
|
||||||
// An in-memory overlay used as a write-cache.
|
|
||||||
overlay: Map<(SessionIndex, CandidateReceipt), CandidateVotes>,
|
|
||||||
highest_session: SessionIndex,
|
highest_session: SessionIndex,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -67,14 +65,14 @@ For each leaf in the leaves update:
|
|||||||
* Fetch the session index for the child of the block with a [`RuntimeApiMessage::SessionIndexForChild`][RuntimeApiMessage].
|
* Fetch the session index for the child of the block with a [`RuntimeApiMessage::SessionIndexForChild`][RuntimeApiMessage].
|
||||||
* If the session index is higher than `state.highest_session`:
|
* If the session index is higher than `state.highest_session`:
|
||||||
* update `state.highest_session`
|
* update `state.highest_session`
|
||||||
* remove everything with session index less than `state.highest_session - DISPUTE_WINDOW` from the overlay and from the `"active-disputes"` in the DB.
|
* remove everything with session index less than `state.highest_session - DISPUTE_WINDOW` from the `"active-disputes"` in the DB.
|
||||||
* Use `iter_with_prefix` to remove everything from `"earliest-session"` up to `state.highest_session - DISPUTE_WINDOW` from the DB under `"candidate-votes"`.
|
* Use `iter_with_prefix` to remove everything from `"earliest-session"` up to `state.highest_session - DISPUTE_WINDOW` from the DB under `"candidate-votes"`.
|
||||||
* Update `"earliest-session"` to be equal to `state.highest_session - DISPUTE_WINDOW`.
|
* Update `"earliest-session"` to be equal to `state.highest_session - DISPUTE_WINDOW`.
|
||||||
* For each new block, explicitly or implicitly, under the new leaf, scan for a dispute digest which indicates a rollback. If a rollback is detected, use the ChainApi subsystem to blacklist the chain.
|
* For each new block, explicitly or implicitly, under the new leaf, scan for a dispute digest which indicates a rollback. If a rollback is detected, use the ChainApi subsystem to blacklist the chain.
|
||||||
|
|
||||||
### On `OverseerSignal::Conclude`
|
### On `OverseerSignal::Conclude`
|
||||||
|
|
||||||
Flush the overlay to DB and conclude.
|
Exit gracefully.
|
||||||
|
|
||||||
### On `OverseerSignal::BlockFinalized`
|
### On `OverseerSignal::BlockFinalized`
|
||||||
|
|
||||||
@@ -84,11 +82,11 @@ Do nothing.
|
|||||||
|
|
||||||
* Deconstruct into parts `{ candidate_hash, candidate_receipt, session, statements }`.
|
* Deconstruct into parts `{ candidate_hash, candidate_receipt, session, statements }`.
|
||||||
* If the session is earlier than `state.highest_session - DISPUTE_WINDOW`, return.
|
* If the session is earlier than `state.highest_session - DISPUTE_WINDOW`, return.
|
||||||
* If there is an entry in the `state.overlay`, load that. Otherwise, load from underlying DB by querying `(session, "candidate-votes", candidate_hash). If that does not exist, create fresh with the given candidate receipt.
|
* Load from underlying DB by querying `("candidate-votes", session, candidate_hash). If that does not exist, create fresh with the given candidate receipt.
|
||||||
* If candidate votes is empty and the statements only contain dispute-specific votes, return.
|
* If candidate votes is empty and the statements only contain dispute-specific votes, return.
|
||||||
* Otherwise, if there is already an entry from the validator in the respective `valid` or `invalid` field of the `CandidateVotes`, return.
|
* Otherwise, if there is already an entry from the validator in the respective `valid` or `invalid` field of the `CandidateVotes`, return.
|
||||||
* Add an entry to the respective `valid` or `invalid` list of the `CandidateVotes` for each statement in `statements`.
|
* Add an entry to the respective `valid` or `invalid` list of the `CandidateVotes` for each statement in `statements`.
|
||||||
* Write the `CandidateVotes` to the `state.overlay`.
|
* Write the `CandidateVotes` to the underyling DB.
|
||||||
* If the both `valid` and `invalid` lists now have non-zero length where previously one or both had zero length, the candidate is now freshly disputed.
|
* If the both `valid` and `invalid` lists now have non-zero length where previously one or both had zero length, the candidate is now freshly disputed.
|
||||||
* If freshly disputed, load `"active-disputes"` and add the candidate hash and session index. Also issue a [`DisputeParticipationMessage::Participate`][DisputeParticipationMessage].
|
* If freshly disputed, load `"active-disputes"` and add the candidate hash and session index. Also issue a [`DisputeParticipationMessage::Participate`][DisputeParticipationMessage].
|
||||||
* If the dispute now has supermajority votes in the "valid" direction, according to the `SessionInfo` of the dispute candidate's session, remove from `"active-disputes"`.
|
* If the dispute now has supermajority votes in the "valid" direction, according to the `SessionInfo` of the dispute candidate's session, remove from `"active-disputes"`.
|
||||||
@@ -101,8 +99,7 @@ Do nothing.
|
|||||||
|
|
||||||
### On `DisputeCoordinatorMessage::QueryCandidateVotes`
|
### On `DisputeCoordinatorMessage::QueryCandidateVotes`
|
||||||
|
|
||||||
* Load from the `state.overlay`, and return the data if `Some`.
|
* Load `"candidate-votes"` and return the data within or `None` if missing.
|
||||||
* Otherwise, load `"candidate-votes"` and return the data within or `None` if missing.
|
|
||||||
|
|
||||||
### On `DisputeCoordinatorMessage::IssueLocalStatement`
|
### On `DisputeCoordinatorMessage::IssueLocalStatement`
|
||||||
|
|
||||||
@@ -119,11 +116,6 @@ Do nothing.
|
|||||||
1. If there is a dispute, exit the loop.
|
1. If there is a dispute, exit the loop.
|
||||||
* For the highest index `i` reached in the `block_descriptions`, send `(base_number + i + 1, block_hash)` on the channel, unless `i` is 0, in which case `None` should be sent. The `block_hash` is determined by inspecting `block_descriptions[i]`.
|
* For the highest index `i` reached in the `block_descriptions`, send `(base_number + i + 1, block_hash)` on the channel, unless `i` is 0, in which case `None` should be sent. The `block_hash` is determined by inspecting `block_descriptions[i]`.
|
||||||
|
|
||||||
### Periodically
|
|
||||||
|
|
||||||
* Flush the `state.overlay` to the DB, writing all entries within
|
|
||||||
* Clear `state.overlay`.
|
|
||||||
|
|
||||||
[DisputeTypes]: ../../types/disputes.md
|
[DisputeTypes]: ../../types/disputes.md
|
||||||
[DisputeStatement]: ../../types/disputes.md#disputestatement
|
[DisputeStatement]: ../../types/disputes.md#disputestatement
|
||||||
[DisputeCoordinatorMessage]: ../../types/overseer-protocol.md#dispute-coordinator-message
|
[DisputeCoordinatorMessage]: ../../types/overseer-protocol.md#dispute-coordinator-message
|
||||||
|
|||||||
Reference in New Issue
Block a user