approval-voting improvement: include all tranche0 assignments in one certificate (#1178)

**_PR migrated from https://github.com/paritytech/polkadot/pull/6782_** 

This PR will upgrade the network protocol to version 3 -> VStaging which
will later be renamed to V3. This version introduces a new kind of
assignment certificate that will be used for tranche0 assignments.
Instead of issuing/importing one tranche0 assignment per candidate,
there will be just one certificate per relay chain block per validator.
However, we will not be sending out the new assignment certificates,
yet. So everything should work exactly as before. Once the majority of
the validators have been upgraded to the new protocol version we will
enable the new certificates (starting at a specific relay chain block)
with a new client update.

There are still a few things that need to be done:

- [x] Use bitfield instead of Vec<CandidateIndex>:
https://github.com/paritytech/polkadot/pull/6802
  - [x] Fix existing approval-distribution and approval-voting tests
  - [x] Fix bitfield-distribution and statement-distribution tests
  - [x] Fix network bridge tests
  - [x] Implement todos in the code
  - [x] Add tests to cover new code
  - [x] Update metrics
  - [x] Remove the approval distribution aggression levels: TBD PR
  - [x] Parachains DB migration 
  - [x] Test network protocol upgrade on Versi
  - [x] Versi Load test
  - [x] Add Zombienet test
  - [x] Documentation updates
- [x] Fix for sending DistributeAssignment for each candidate claimed by
a v2 assignment (warning: Importing locally an already known assignment)
 - [x]  Fix AcceptedDuplicate
 - [x] Fix DB migration so that we can still keep old data.
 - [x] Final Versi burn in

---------

Signed-off-by: Andrei Sandu <andrei-mihail@parity.io>
Signed-off-by: Alexandru Gheorghe <alexandru.gheorghe@parity.io>
Co-authored-by: Alexandru Gheorghe <alexandru.gheorghe@parity.io>
This commit is contained in:
Andrei Sandu
2023-11-06 15:21:32 +02:00
committed by GitHub
parent 4ac9c4a364
commit 0570b6fa9e
55 changed files with 5643 additions and 1799 deletions
+8
View File
@@ -105,6 +105,14 @@ zombienet-polkadot-functional-0005-parachains-disputes-past-session:
--local-dir="${LOCAL_DIR}/functional"
--test="0005-parachains-disputes-past-session.zndsl"
zombienet-polkadot-functional-0006-parachains-max-tranche0:
extends:
- .zombienet-polkadot-common
script:
- /home/nonroot/zombie-net/scripts/ci/run-test-local-env-manager.sh
--local-dir="${LOCAL_DIR}/functional"
--test="0006-parachains-max-tranche0.zndsl"
zombienet-polkadot-smoke-0001-parachains-smoke-test:
extends:
- .zombienet-polkadot-common
Generated
+10
View File
@@ -11761,9 +11761,11 @@ name = "polkadot-approval-distribution"
version = "1.0.0"
dependencies = [
"assert_matches",
"bitvec",
"env_logger 0.9.3",
"futures",
"futures-timer",
"itertools 0.10.5",
"log",
"polkadot-node-jaeger",
"polkadot-node-metrics",
@@ -12071,10 +12073,13 @@ dependencies = [
"async-trait",
"bitvec",
"derive_more",
"env_logger 0.9.3",
"futures",
"futures-timer",
"itertools 0.10.5",
"kvdb",
"kvdb-memorydb",
"log",
"merlin 2.0.1",
"parity-scale-codec",
"parking_lot 0.12.1",
@@ -12086,6 +12091,8 @@ dependencies = [
"polkadot-overseer",
"polkadot-primitives",
"polkadot-primitives-test-helpers",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand_core 0.5.1",
"sc-keystore",
"schnellru",
@@ -12541,6 +12548,7 @@ dependencies = [
name = "polkadot-node-primitives"
version = "1.0.0"
dependencies = [
"bitvec",
"bounded-vec",
"futures",
"parity-scale-codec",
@@ -12592,6 +12600,7 @@ name = "polkadot-node-subsystem-types"
version = "1.0.0"
dependencies = [
"async-trait",
"bitvec",
"derive_more",
"futures",
"orchestra",
@@ -13033,6 +13042,7 @@ dependencies = [
"polkadot-overseer",
"polkadot-parachain-primitives",
"polkadot-primitives",
"polkadot-primitives-test-helpers",
"polkadot-rpc",
"polkadot-runtime-parachains",
"polkadot-statement-distribution",
+2
View File
@@ -64,6 +64,8 @@ jemalloc-allocator = [
"polkadot-node-core-pvf/jemalloc-allocator",
"polkadot-overseer/jemalloc-allocator",
]
network-protocol-staging = [ "polkadot-cli/network-protocol-staging" ]
# Enables timeout-based tests supposed to be run only in CI environment as they may be flaky
# when run locally depending on system load
+2
View File
@@ -74,3 +74,5 @@ runtime-metrics = [
"polkadot-node-metrics/runtime-metrics",
"service/runtime-metrics",
]
network-protocol-staging = [ "service/network-protocol-staging" ]
@@ -17,6 +17,7 @@ schnorrkel = "0.9.1"
kvdb = "0.13.0"
derive_more = "0.99.17"
thiserror = "1.0.48"
itertools = "0.10.5"
polkadot-node-subsystem = { path = "../../subsystem" }
polkadot-node-subsystem-util = { path = "../../subsystem-util" }
@@ -30,6 +31,9 @@ sp-consensus = { path = "../../../../substrate/primitives/consensus/common", def
sp-consensus-slots = { path = "../../../../substrate/primitives/consensus/slots", default-features = false }
sp-application-crypto = { path = "../../../../substrate/primitives/application-crypto", default-features = false, features = ["full_crypto"] }
sp-runtime = { path = "../../../../substrate/primitives/runtime", default-features = false }
rand_core = "0.5.1"
rand_chacha = { version = "0.3.1" }
rand = "0.8.5"
[dev-dependencies]
async-trait = "0.1.57"
@@ -43,3 +47,5 @@ polkadot-node-subsystem-test-helpers = { path = "../../subsystem-test-helpers" }
assert_matches = "1.4.0"
kvdb-memorydb = "0.13.0"
test-helpers = { package = "polkadot-primitives-test-helpers", path = "../../../primitives/test-helpers" }
log = "0.4.17"
env_logger = "0.9.0"
@@ -17,7 +17,7 @@
//! Utilities for checking whether a candidate has been approved under a given block.
use bitvec::{order::Lsb0 as BitOrderLsb0, slice::BitSlice};
use polkadot_node_primitives::approval::DelayTranche;
use polkadot_node_primitives::approval::v1::DelayTranche;
use polkadot_primitives::ValidatorIndex;
use crate::{
@@ -472,9 +472,9 @@ mod tests {
}
.into();
let approval_entry = approval_db::v1::ApprovalEntry {
let approval_entry = approval_db::v2::ApprovalEntry {
tranches: Vec::new(),
assignments: BitVec::default(),
assigned_validators: BitVec::default(),
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
@@ -509,22 +509,22 @@ mod tests {
candidate.mark_approval(ValidatorIndex(i));
}
let approval_entry = approval_db::v1::ApprovalEntry {
let approval_entry = approval_db::v2::ApprovalEntry {
tranches: vec![
approval_db::v1::TrancheEntry {
approval_db::v2::TrancheEntry {
tranche: 0,
assignments: (0..2).map(|i| (ValidatorIndex(i), 0.into())).collect(),
},
approval_db::v1::TrancheEntry {
approval_db::v2::TrancheEntry {
tranche: 1,
assignments: (2..5).map(|i| (ValidatorIndex(i), 1.into())).collect(),
},
approval_db::v1::TrancheEntry {
approval_db::v2::TrancheEntry {
tranche: 2,
assignments: (5..10).map(|i| (ValidatorIndex(i), 0.into())).collect(),
},
],
assignments: bitvec![u8, BitOrderLsb0; 1; 10],
assigned_validators: bitvec![u8, BitOrderLsb0; 1; 10],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
@@ -581,22 +581,22 @@ mod tests {
candidate.mark_approval(ValidatorIndex(i));
}
let approval_entry = approval_db::v1::ApprovalEntry {
let approval_entry = approval_db::v2::ApprovalEntry {
tranches: vec![
approval_db::v1::TrancheEntry {
approval_db::v2::TrancheEntry {
tranche: 0,
assignments: (0..4).map(|i| (ValidatorIndex(i), 0.into())).collect(),
},
approval_db::v1::TrancheEntry {
approval_db::v2::TrancheEntry {
tranche: 1,
assignments: (4..6).map(|i| (ValidatorIndex(i), 1.into())).collect(),
},
approval_db::v1::TrancheEntry {
approval_db::v2::TrancheEntry {
tranche: 2,
assignments: (6..10).map(|i| (ValidatorIndex(i), 0.into())).collect(),
},
],
assignments: bitvec![u8, BitOrderLsb0; 1; 10],
assigned_validators: bitvec![u8, BitOrderLsb0; 1; 10],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
@@ -647,9 +647,9 @@ mod tests {
let no_show_duration = 10;
let needed_approvals = 4;
let mut approval_entry: ApprovalEntry = approval_db::v1::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
tranches: Vec::new(),
assignments: bitvec![u8, BitOrderLsb0; 0; 5],
assigned_validators: bitvec![u8, BitOrderLsb0; 0; 5],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
@@ -691,9 +691,9 @@ mod tests {
let no_show_duration = 10;
let needed_approvals = 4;
let mut approval_entry: ApprovalEntry = approval_db::v1::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
tranches: Vec::new(),
assignments: bitvec![u8, BitOrderLsb0; 0; 10],
assigned_validators: bitvec![u8, BitOrderLsb0; 0; 10],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
@@ -731,9 +731,9 @@ mod tests {
let no_show_duration = 10;
let needed_approvals = 4;
let mut approval_entry: ApprovalEntry = approval_db::v1::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
tranches: Vec::new(),
assignments: bitvec![u8, BitOrderLsb0; 0; 10],
assigned_validators: bitvec![u8, BitOrderLsb0; 0; 10],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
@@ -776,9 +776,9 @@ mod tests {
let needed_approvals = 4;
let n_validators = 8;
let mut approval_entry: ApprovalEntry = approval_db::v1::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
tranches: Vec::new(),
assignments: bitvec![u8, BitOrderLsb0; 0; n_validators],
assigned_validators: bitvec![u8, BitOrderLsb0; 0; n_validators],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
@@ -843,9 +843,9 @@ mod tests {
let needed_approvals = 4;
let n_validators = 8;
let mut approval_entry: ApprovalEntry = approval_db::v1::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
tranches: Vec::new(),
assignments: bitvec![u8, BitOrderLsb0; 0; n_validators],
assigned_validators: bitvec![u8, BitOrderLsb0; 0; n_validators],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
@@ -934,9 +934,9 @@ mod tests {
let needed_approvals = 4;
let n_validators = 8;
let mut approval_entry: ApprovalEntry = approval_db::v1::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
tranches: Vec::new(),
assignments: bitvec![u8, BitOrderLsb0; 0; n_validators],
assigned_validators: bitvec![u8, BitOrderLsb0; 0; n_validators],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
@@ -1041,15 +1041,15 @@ mod tests {
candidate.mark_approval(ValidatorIndex(i));
}
let approval_entry = approval_db::v1::ApprovalEntry {
let approval_entry = approval_db::v2::ApprovalEntry {
tranches: vec![
// Assignments with invalid validator indexes.
approval_db::v1::TrancheEntry {
approval_db::v2::TrancheEntry {
tranche: 1,
assignments: (2..5).map(|i| (ValidatorIndex(i), 1.into())).collect(),
},
],
assignments: bitvec![u8, BitOrderLsb0; 1; 3],
assigned_validators: bitvec![u8, BitOrderLsb0; 1; 3],
our_assignment: None,
our_approval_sig: None,
backing_group: GroupIndex(0),
@@ -1094,12 +1094,12 @@ mod tests {
];
for test_tranche in test_tranches {
let mut approval_entry: ApprovalEntry = approval_db::v1::ApprovalEntry {
let mut approval_entry: ApprovalEntry = approval_db::v2::ApprovalEntry {
tranches: Vec::new(),
backing_group: GroupIndex(0),
our_assignment: None,
our_approval_sig: None,
assignments: bitvec![u8, BitOrderLsb0; 0; 3],
assigned_validators: bitvec![u8, BitOrderLsb0; 0; 3],
approved: false,
}
.into();
@@ -31,3 +31,4 @@
//! time being we share the same DB with the rest of Substrate.
pub mod v1;
pub mod v2;
@@ -23,144 +23,15 @@
//! require a db migration (check `node/service/src/parachains_db/upgrade.rs`).
use parity_scale_codec::{Decode, Encode};
use polkadot_node_primitives::approval::{AssignmentCert, DelayTranche};
use polkadot_node_subsystem::{SubsystemError, SubsystemResult};
use polkadot_node_subsystem_util::database::{DBTransaction, Database};
use polkadot_node_primitives::approval::v1::{AssignmentCert, DelayTranche};
use polkadot_primitives::{
BlockNumber, CandidateHash, CandidateReceipt, CoreIndex, GroupIndex, Hash, SessionIndex,
ValidatorIndex, ValidatorSignature,
};
use sp_consensus_slots::Slot;
use std::collections::BTreeMap;
use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec};
use std::{collections::BTreeMap, sync::Arc};
use crate::{
backend::{Backend, BackendWriteOp},
persisted_entries,
};
const STORED_BLOCKS_KEY: &[u8] = b"Approvals_StoredBlocks";
#[cfg(test)]
pub mod tests;
/// `DbBackend` is a concrete implementation of the higher-level Backend trait
pub struct DbBackend {
inner: Arc<dyn Database>,
config: Config,
}
impl DbBackend {
/// Create a new [`DbBackend`] with the supplied key-value store and
/// config.
pub fn new(db: Arc<dyn Database>, config: Config) -> Self {
DbBackend { inner: db, config }
}
}
impl Backend for DbBackend {
fn load_block_entry(
&self,
block_hash: &Hash,
) -> SubsystemResult<Option<persisted_entries::BlockEntry>> {
load_block_entry(&*self.inner, &self.config, block_hash).map(|e| e.map(Into::into))
}
fn load_candidate_entry(
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<persisted_entries::CandidateEntry>> {
load_candidate_entry(&*self.inner, &self.config, candidate_hash).map(|e| e.map(Into::into))
}
fn load_blocks_at_height(&self, block_height: &BlockNumber) -> SubsystemResult<Vec<Hash>> {
load_blocks_at_height(&*self.inner, &self.config, block_height)
}
fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>> {
load_all_blocks(&*self.inner, &self.config)
}
fn load_stored_blocks(&self) -> SubsystemResult<Option<StoredBlockRange>> {
load_stored_blocks(&*self.inner, &self.config)
}
/// Atomically write the list of operations, with later operations taking precedence over prior.
fn write<I>(&mut self, ops: I) -> SubsystemResult<()>
where
I: IntoIterator<Item = BackendWriteOp>,
{
let mut tx = DBTransaction::new();
for op in ops {
match op {
BackendWriteOp::WriteStoredBlockRange(stored_block_range) => {
tx.put_vec(
self.config.col_approval_data,
&STORED_BLOCKS_KEY,
stored_block_range.encode(),
);
},
BackendWriteOp::DeleteStoredBlockRange => {
tx.delete(self.config.col_approval_data, &STORED_BLOCKS_KEY);
},
BackendWriteOp::WriteBlocksAtHeight(h, blocks) => {
tx.put_vec(
self.config.col_approval_data,
&blocks_at_height_key(h),
blocks.encode(),
);
},
BackendWriteOp::DeleteBlocksAtHeight(h) => {
tx.delete(self.config.col_approval_data, &blocks_at_height_key(h));
},
BackendWriteOp::WriteBlockEntry(block_entry) => {
let block_entry: BlockEntry = block_entry.into();
tx.put_vec(
self.config.col_approval_data,
&block_entry_key(&block_entry.block_hash),
block_entry.encode(),
);
},
BackendWriteOp::DeleteBlockEntry(hash) => {
tx.delete(self.config.col_approval_data, &block_entry_key(&hash));
},
BackendWriteOp::WriteCandidateEntry(candidate_entry) => {
let candidate_entry: CandidateEntry = candidate_entry.into();
tx.put_vec(
self.config.col_approval_data,
&candidate_entry_key(&candidate_entry.candidate.hash()),
candidate_entry.encode(),
);
},
BackendWriteOp::DeleteCandidateEntry(candidate_hash) => {
tx.delete(self.config.col_approval_data, &candidate_entry_key(&candidate_hash));
},
}
}
self.inner.write(tx).map_err(|e| e.into())
}
}
/// A range from earliest..last block number stored within the DB.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct StoredBlockRange(pub BlockNumber, pub BlockNumber);
// slot_duration * 2 + DelayTranche gives the number of delay tranches since the
// unix epoch.
#[derive(Encode, Decode, Clone, Copy, Debug, PartialEq)]
pub struct Tick(u64);
/// Convenience type definition
pub type Bitfield = BitVec<u8, BitOrderLsb0>;
/// The database config.
#[derive(Debug, Clone, Copy)]
pub struct Config {
/// The column family in the database where data is stored.
pub col_approval_data: u32,
}
use super::v2::Bitfield;
/// Details pertaining to our assignment on a block.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
@@ -171,15 +42,7 @@ pub struct OurAssignment {
// Whether the assignment has been triggered already.
pub triggered: bool,
}
/// Metadata regarding a specific tranche of assignments for a specific candidate.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct TrancheEntry {
pub tranche: DelayTranche,
// Assigned validators, and the instant we received their assignment, rounded
// to the nearest tick.
pub assignments: Vec<(ValidatorIndex, Tick)>,
}
use super::v2::TrancheEntry;
/// Metadata regarding approval of a particular candidate within the context of some
/// particular block.
@@ -226,126 +89,3 @@ pub struct BlockEntry {
pub approved_bitfield: Bitfield,
pub children: Vec<Hash>,
}
impl From<crate::Tick> for Tick {
fn from(tick: crate::Tick) -> Tick {
Tick(tick)
}
}
impl From<Tick> for crate::Tick {
fn from(tick: Tick) -> crate::Tick {
tick.0
}
}
/// Errors while accessing things from the DB.
#[derive(Debug, derive_more::From, derive_more::Display)]
pub enum Error {
Io(std::io::Error),
InvalidDecoding(parity_scale_codec::Error),
}
impl std::error::Error for Error {}
/// Result alias for DB errors.
pub type Result<T> = std::result::Result<T, Error>;
pub(crate) fn load_decode<D: Decode>(
store: &dyn Database,
col_approval_data: u32,
key: &[u8],
) -> Result<Option<D>> {
match store.get(col_approval_data, key)? {
None => Ok(None),
Some(raw) => D::decode(&mut &raw[..]).map(Some).map_err(Into::into),
}
}
/// The key a given block entry is stored under.
pub(crate) fn block_entry_key(block_hash: &Hash) -> [u8; 46] {
const BLOCK_ENTRY_PREFIX: [u8; 14] = *b"Approvals_blck";
let mut key = [0u8; 14 + 32];
key[0..14].copy_from_slice(&BLOCK_ENTRY_PREFIX);
key[14..][..32].copy_from_slice(block_hash.as_ref());
key
}
/// The key a given candidate entry is stored under.
pub(crate) fn candidate_entry_key(candidate_hash: &CandidateHash) -> [u8; 46] {
const CANDIDATE_ENTRY_PREFIX: [u8; 14] = *b"Approvals_cand";
let mut key = [0u8; 14 + 32];
key[0..14].copy_from_slice(&CANDIDATE_ENTRY_PREFIX);
key[14..][..32].copy_from_slice(candidate_hash.0.as_ref());
key
}
/// The key a set of block hashes corresponding to a block number is stored under.
pub(crate) fn blocks_at_height_key(block_number: BlockNumber) -> [u8; 16] {
const BLOCKS_AT_HEIGHT_PREFIX: [u8; 12] = *b"Approvals_at";
let mut key = [0u8; 12 + 4];
key[0..12].copy_from_slice(&BLOCKS_AT_HEIGHT_PREFIX);
block_number.using_encoded(|s| key[12..16].copy_from_slice(s));
key
}
/// Return all blocks which have entries in the DB, ascending, by height.
pub fn load_all_blocks(store: &dyn Database, config: &Config) -> SubsystemResult<Vec<Hash>> {
let mut hashes = Vec::new();
if let Some(stored_blocks) = load_stored_blocks(store, config)? {
for height in stored_blocks.0..stored_blocks.1 {
let blocks = load_blocks_at_height(store, config, &height)?;
hashes.extend(blocks);
}
}
Ok(hashes)
}
/// Load the stored-blocks key from the state.
pub fn load_stored_blocks(
store: &dyn Database,
config: &Config,
) -> SubsystemResult<Option<StoredBlockRange>> {
load_decode(store, config.col_approval_data, STORED_BLOCKS_KEY)
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a blocks-at-height entry for a given block number.
pub fn load_blocks_at_height(
store: &dyn Database,
config: &Config,
block_number: &BlockNumber,
) -> SubsystemResult<Vec<Hash>> {
load_decode(store, config.col_approval_data, &blocks_at_height_key(*block_number))
.map(|x| x.unwrap_or_default())
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a block entry from the aux store.
pub fn load_block_entry(
store: &dyn Database,
config: &Config,
block_hash: &Hash,
) -> SubsystemResult<Option<BlockEntry>> {
load_decode(store, config.col_approval_data, &block_entry_key(block_hash))
.map(|u: Option<BlockEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a candidate entry from the aux store.
pub fn load_candidate_entry(
store: &dyn Database,
config: &Config,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>> {
load_decode(store, config.col_approval_data, &candidate_entry_key(candidate_hash))
.map(|u: Option<CandidateEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
@@ -0,0 +1,244 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Approval DB migration helpers.
use super::*;
use crate::backend::Backend;
use polkadot_node_primitives::approval::v1::{
AssignmentCert, AssignmentCertKind, VrfOutput, VrfProof, VrfSignature, RELAY_VRF_MODULO_CONTEXT,
};
use polkadot_node_subsystem_util::database::Database;
use sp_application_crypto::sp_core::H256;
use std::{collections::HashSet, sync::Arc};
fn dummy_assignment_cert(kind: AssignmentCertKind) -> AssignmentCert {
let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT);
let msg = b"test-garbage";
let mut prng = rand_core::OsRng;
let keypair = schnorrkel::Keypair::generate_with(&mut prng);
let (inout, proof, _) = keypair.vrf_sign(ctx.bytes(msg));
let out = inout.to_output();
AssignmentCert { kind, vrf: VrfSignature { output: VrfOutput(out), proof: VrfProof(proof) } }
}
fn make_block_entry_v1(
block_hash: Hash,
parent_hash: Hash,
block_number: BlockNumber,
candidates: Vec<(CoreIndex, CandidateHash)>,
) -> crate::approval_db::v1::BlockEntry {
crate::approval_db::v1::BlockEntry {
block_hash,
parent_hash,
block_number,
session: 1,
slot: Slot::from(1),
relay_vrf_story: [0u8; 32],
approved_bitfield: make_bitvec(candidates.len()),
candidates,
children: Vec::new(),
}
}
fn make_bitvec(len: usize) -> BitVec<u8, BitOrderLsb0> {
bitvec::bitvec![u8, BitOrderLsb0; 0; len]
}
/// Migrates `OurAssignment`, `CandidateEntry` and `ApprovalEntry` to version 2.
/// Returns on any error.
/// Must only be used in parachains DB migration code - `polkadot-service` crate.
pub fn v1_to_v2(db: Arc<dyn Database>, config: Config) -> Result<()> {
let mut backend = crate::DbBackend::new(db, config);
let all_blocks = backend
.load_all_blocks()
.map_err(|e| Error::InternalError(e))?
.iter()
.filter_map(|block_hash| {
backend
.load_block_entry_v1(block_hash)
.map_err(|e| Error::InternalError(e))
.ok()?
})
.collect::<Vec<_>>();
gum::info!(
target: crate::LOG_TARGET,
"Migrating candidate entries on top of {} blocks",
all_blocks.len()
);
let mut overlay = crate::OverlayedBackend::new(&backend);
let mut counter = 0;
// Get all candidate entries, approval entries and convert each of them.
for block in all_blocks {
for (_core_index, candidate_hash) in block.candidates() {
// Loading the candidate will also perform the conversion to the updated format and
// return that represantation.
if let Some(candidate_entry) = backend
.load_candidate_entry_v1(&candidate_hash)
.map_err(|e| Error::InternalError(e))?
{
// Write the updated representation.
overlay.write_candidate_entry(candidate_entry);
counter += 1;
}
}
overlay.write_block_entry(block);
}
gum::info!(target: crate::LOG_TARGET, "Migrated {} entries", counter);
// Commit all changes to DB.
let write_ops = overlay.into_write_ops();
backend.write(write_ops).unwrap();
Ok(())
}
// Checks if the migration doesn't leave the DB in an unsane state.
// This function is to be used in tests.
pub fn v1_to_v2_sanity_check(
db: Arc<dyn Database>,
config: Config,
expected_candidates: HashSet<CandidateHash>,
) -> Result<()> {
let backend = crate::DbBackend::new(db, config);
let all_blocks = backend
.load_all_blocks()
.unwrap()
.iter()
.map(|block_hash| backend.load_block_entry(block_hash).unwrap().unwrap())
.collect::<Vec<_>>();
let mut candidates = HashSet::new();
// Iterate all blocks and approval entries.
for block in all_blocks {
for (_core_index, candidate_hash) in block.candidates() {
// Loading the candidate will also perform the conversion to the updated format and
// return that represantation.
if let Some(candidate_entry) = backend.load_candidate_entry(&candidate_hash).unwrap() {
candidates.insert(candidate_entry.candidate.hash());
}
}
}
assert_eq!(candidates, expected_candidates);
Ok(())
}
// Fills the db with dummy data in v1 scheme.
pub fn v1_to_v2_fill_test_data<F>(
db: Arc<dyn Database>,
config: Config,
dummy_candidate_create: F,
) -> Result<HashSet<CandidateHash>>
where
F: Fn(H256) -> CandidateReceipt<H256>,
{
let mut backend = crate::DbBackend::new(db.clone(), config);
let mut overlay_db = crate::OverlayedBackend::new(&backend);
let mut expected_candidates = HashSet::new();
const RELAY_BLOCK_COUNT: u32 = 10;
let range = StoredBlockRange(1, 11);
overlay_db.write_stored_block_range(range.clone());
for relay_number in 1..=RELAY_BLOCK_COUNT {
let relay_hash = Hash::repeat_byte(relay_number as u8);
let assignment_core_index = CoreIndex(relay_number);
let candidate = dummy_candidate_create(relay_hash);
let candidate_hash = candidate.hash();
let at_height = vec![relay_hash];
let block_entry = make_block_entry_v1(
relay_hash,
Default::default(),
relay_number,
vec![(assignment_core_index, candidate_hash)],
);
let dummy_assignment = crate::approval_db::v1::OurAssignment {
cert: dummy_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }).into(),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
};
let candidate_entry = crate::approval_db::v1::CandidateEntry {
candidate,
session: 123,
block_assignments: vec![(
relay_hash,
crate::approval_db::v1::ApprovalEntry {
tranches: Vec::new(),
backing_group: GroupIndex(1),
our_assignment: Some(dummy_assignment),
our_approval_sig: None,
assignments: Default::default(),
approved: false,
},
)]
.into_iter()
.collect(),
approvals: Default::default(),
};
overlay_db.write_blocks_at_height(relay_number, at_height.clone());
expected_candidates.insert(candidate_entry.candidate.hash());
db.write(write_candidate_entry_v1(candidate_entry, config)).unwrap();
db.write(write_block_entry_v1(block_entry, config)).unwrap();
}
let write_ops = overlay_db.into_write_ops();
backend.write(write_ops).unwrap();
Ok(expected_candidates)
}
// Low level DB helper to write a candidate entry in v1 scheme.
fn write_candidate_entry_v1(
candidate_entry: crate::approval_db::v1::CandidateEntry,
config: Config,
) -> DBTransaction {
let mut tx = DBTransaction::new();
tx.put_vec(
config.col_approval_data,
&candidate_entry_key(&candidate_entry.candidate.hash()),
candidate_entry.encode(),
);
tx
}
// Low level DB helper to write a block entry in v1 scheme.
fn write_block_entry_v1(
block_entry: crate::approval_db::v1::BlockEntry,
config: Config,
) -> DBTransaction {
let mut tx = DBTransaction::new();
tx.put_vec(
config.col_approval_data,
&block_entry_key(&block_entry.block_hash),
block_entry.encode(),
);
tx
}
@@ -0,0 +1,394 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Version 2 of the DB schema.
use parity_scale_codec::{Decode, Encode};
use polkadot_node_primitives::approval::{v1::DelayTranche, v2::AssignmentCertV2};
use polkadot_node_subsystem::{SubsystemError, SubsystemResult};
use polkadot_node_subsystem_util::database::{DBTransaction, Database};
use polkadot_primitives::{
BlockNumber, CandidateHash, CandidateReceipt, CoreIndex, GroupIndex, Hash, SessionIndex,
ValidatorIndex, ValidatorSignature,
};
use sp_consensus_slots::Slot;
use bitvec::{order::Lsb0 as BitOrderLsb0, vec::BitVec};
use std::{collections::BTreeMap, sync::Arc};
use crate::{
backend::{Backend, BackendWriteOp, V1ReadBackend},
persisted_entries,
};
const STORED_BLOCKS_KEY: &[u8] = b"Approvals_StoredBlocks";
pub mod migration_helpers;
#[cfg(test)]
pub mod tests;
/// `DbBackend` is a concrete implementation of the higher-level Backend trait
pub struct DbBackend {
inner: Arc<dyn Database>,
config: Config,
}
impl DbBackend {
/// Create a new [`DbBackend`] with the supplied key-value store and
/// config.
pub fn new(db: Arc<dyn Database>, config: Config) -> Self {
DbBackend { inner: db, config }
}
}
impl V1ReadBackend for DbBackend {
fn load_candidate_entry_v1(
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<persisted_entries::CandidateEntry>> {
load_candidate_entry_v1(&*self.inner, &self.config, candidate_hash)
.map(|e| e.map(Into::into))
}
fn load_block_entry_v1(
&self,
block_hash: &Hash,
) -> SubsystemResult<Option<persisted_entries::BlockEntry>> {
load_block_entry_v1(&*self.inner, &self.config, block_hash).map(|e| e.map(Into::into))
}
}
impl Backend for DbBackend {
fn load_block_entry(
&self,
block_hash: &Hash,
) -> SubsystemResult<Option<persisted_entries::BlockEntry>> {
load_block_entry(&*self.inner, &self.config, block_hash).map(|e| e.map(Into::into))
}
fn load_candidate_entry(
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<persisted_entries::CandidateEntry>> {
load_candidate_entry(&*self.inner, &self.config, candidate_hash).map(|e| e.map(Into::into))
}
fn load_blocks_at_height(&self, block_height: &BlockNumber) -> SubsystemResult<Vec<Hash>> {
load_blocks_at_height(&*self.inner, &self.config, block_height)
}
fn load_all_blocks(&self) -> SubsystemResult<Vec<Hash>> {
load_all_blocks(&*self.inner, &self.config)
}
fn load_stored_blocks(&self) -> SubsystemResult<Option<StoredBlockRange>> {
load_stored_blocks(&*self.inner, &self.config)
}
/// Atomically write the list of operations, with later operations taking precedence over prior.
fn write<I>(&mut self, ops: I) -> SubsystemResult<()>
where
I: IntoIterator<Item = BackendWriteOp>,
{
let mut tx = DBTransaction::new();
for op in ops {
match op {
BackendWriteOp::WriteStoredBlockRange(stored_block_range) => {
tx.put_vec(
self.config.col_approval_data,
&STORED_BLOCKS_KEY,
stored_block_range.encode(),
);
},
BackendWriteOp::DeleteStoredBlockRange => {
tx.delete(self.config.col_approval_data, &STORED_BLOCKS_KEY);
},
BackendWriteOp::WriteBlocksAtHeight(h, blocks) => {
tx.put_vec(
self.config.col_approval_data,
&blocks_at_height_key(h),
blocks.encode(),
);
},
BackendWriteOp::DeleteBlocksAtHeight(h) => {
tx.delete(self.config.col_approval_data, &blocks_at_height_key(h));
},
BackendWriteOp::WriteBlockEntry(block_entry) => {
let block_entry: BlockEntry = block_entry.into();
tx.put_vec(
self.config.col_approval_data,
&block_entry_key(&block_entry.block_hash),
block_entry.encode(),
);
},
BackendWriteOp::DeleteBlockEntry(hash) => {
tx.delete(self.config.col_approval_data, &block_entry_key(&hash));
},
BackendWriteOp::WriteCandidateEntry(candidate_entry) => {
let candidate_entry: CandidateEntry = candidate_entry.into();
tx.put_vec(
self.config.col_approval_data,
&candidate_entry_key(&candidate_entry.candidate.hash()),
candidate_entry.encode(),
);
},
BackendWriteOp::DeleteCandidateEntry(candidate_hash) => {
tx.delete(self.config.col_approval_data, &candidate_entry_key(&candidate_hash));
},
}
}
self.inner.write(tx).map_err(|e| e.into())
}
}
/// A range from earliest..last block number stored within the DB.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct StoredBlockRange(pub BlockNumber, pub BlockNumber);
// slot_duration * 2 + DelayTranche gives the number of delay tranches since the
// unix epoch.
#[derive(Encode, Decode, Clone, Copy, Debug, PartialEq)]
pub struct Tick(u64);
/// Convenience type definition
pub type Bitfield = BitVec<u8, BitOrderLsb0>;
/// The database config.
#[derive(Debug, Clone, Copy)]
pub struct Config {
/// The column family in the database where data is stored.
pub col_approval_data: u32,
}
/// Details pertaining to our assignment on a block.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct OurAssignment {
/// Our assignment certificate.
pub cert: AssignmentCertV2,
/// The tranche for which the assignment refers to.
pub tranche: DelayTranche,
/// Our validator index for the session in which the candidates were included.
pub validator_index: ValidatorIndex,
/// Whether the assignment has been triggered already.
pub triggered: bool,
}
/// Metadata regarding a specific tranche of assignments for a specific candidate.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct TrancheEntry {
pub tranche: DelayTranche,
// Assigned validators, and the instant we received their assignment, rounded
// to the nearest tick.
pub assignments: Vec<(ValidatorIndex, Tick)>,
}
/// Metadata regarding approval of a particular candidate within the context of some
/// particular block.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct ApprovalEntry {
pub tranches: Vec<TrancheEntry>,
pub backing_group: GroupIndex,
pub our_assignment: Option<OurAssignment>,
pub our_approval_sig: Option<ValidatorSignature>,
// `n_validators` bits.
pub assigned_validators: Bitfield,
pub approved: bool,
}
/// Metadata regarding approval of a particular candidate.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct CandidateEntry {
pub candidate: CandidateReceipt,
pub session: SessionIndex,
// Assignments are based on blocks, so we need to track assignments separately
// based on the block we are looking at.
pub block_assignments: BTreeMap<Hash, ApprovalEntry>,
pub approvals: Bitfield,
}
/// Metadata regarding approval of a particular block, by way of approval of the
/// candidates contained within it.
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub struct BlockEntry {
pub block_hash: Hash,
pub block_number: BlockNumber,
pub parent_hash: Hash,
pub session: SessionIndex,
pub slot: Slot,
/// Random bytes derived from the VRF submitted within the block by the block
/// author as a credential and used as input to approval assignment criteria.
pub relay_vrf_story: [u8; 32],
// The candidates included as-of this block and the index of the core they are
// leaving. Sorted ascending by core index.
pub candidates: Vec<(CoreIndex, CandidateHash)>,
// A bitfield where the i'th bit corresponds to the i'th candidate in `candidates`.
// The i'th bit is `true` iff the candidate has been approved in the context of this
// block. The block can be considered approved if the bitfield has all bits set to `true`.
pub approved_bitfield: Bitfield,
pub children: Vec<Hash>,
// Assignments we already distributed. A 1 bit means the candidate index for which
// we already have sent out an assignment. We need this to avoid distributing
// multiple core assignments more than once.
pub distributed_assignments: Bitfield,
}
impl From<crate::Tick> for Tick {
fn from(tick: crate::Tick) -> Tick {
Tick(tick)
}
}
impl From<Tick> for crate::Tick {
fn from(tick: Tick) -> crate::Tick {
tick.0
}
}
/// Errors while accessing things from the DB.
#[derive(Debug, derive_more::From, derive_more::Display)]
pub enum Error {
Io(std::io::Error),
InvalidDecoding(parity_scale_codec::Error),
InternalError(SubsystemError),
}
impl std::error::Error for Error {}
/// Result alias for DB errors.
pub type Result<T> = std::result::Result<T, Error>;
pub(crate) fn load_decode<D: Decode>(
store: &dyn Database,
col_approval_data: u32,
key: &[u8],
) -> Result<Option<D>> {
match store.get(col_approval_data, key)? {
None => Ok(None),
Some(raw) => D::decode(&mut &raw[..]).map(Some).map_err(Into::into),
}
}
/// The key a given block entry is stored under.
pub(crate) fn block_entry_key(block_hash: &Hash) -> [u8; 46] {
const BLOCK_ENTRY_PREFIX: [u8; 14] = *b"Approvals_blck";
let mut key = [0u8; 14 + 32];
key[0..14].copy_from_slice(&BLOCK_ENTRY_PREFIX);
key[14..][..32].copy_from_slice(block_hash.as_ref());
key
}
/// The key a given candidate entry is stored under.
pub(crate) fn candidate_entry_key(candidate_hash: &CandidateHash) -> [u8; 46] {
const CANDIDATE_ENTRY_PREFIX: [u8; 14] = *b"Approvals_cand";
let mut key = [0u8; 14 + 32];
key[0..14].copy_from_slice(&CANDIDATE_ENTRY_PREFIX);
key[14..][..32].copy_from_slice(candidate_hash.0.as_ref());
key
}
/// The key a set of block hashes corresponding to a block number is stored under.
pub(crate) fn blocks_at_height_key(block_number: BlockNumber) -> [u8; 16] {
const BLOCKS_AT_HEIGHT_PREFIX: [u8; 12] = *b"Approvals_at";
let mut key = [0u8; 12 + 4];
key[0..12].copy_from_slice(&BLOCKS_AT_HEIGHT_PREFIX);
block_number.using_encoded(|s| key[12..16].copy_from_slice(s));
key
}
/// Return all blocks which have entries in the DB, ascending, by height.
pub fn load_all_blocks(store: &dyn Database, config: &Config) -> SubsystemResult<Vec<Hash>> {
let mut hashes = Vec::new();
if let Some(stored_blocks) = load_stored_blocks(store, config)? {
for height in stored_blocks.0..stored_blocks.1 {
let blocks = load_blocks_at_height(store, config, &height)?;
hashes.extend(blocks);
}
}
Ok(hashes)
}
/// Load the stored-blocks key from the state.
pub fn load_stored_blocks(
store: &dyn Database,
config: &Config,
) -> SubsystemResult<Option<StoredBlockRange>> {
load_decode(store, config.col_approval_data, STORED_BLOCKS_KEY)
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a blocks-at-height entry for a given block number.
pub fn load_blocks_at_height(
store: &dyn Database,
config: &Config,
block_number: &BlockNumber,
) -> SubsystemResult<Vec<Hash>> {
load_decode(store, config.col_approval_data, &blocks_at_height_key(*block_number))
.map(|x| x.unwrap_or_default())
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a block entry from the aux store.
pub fn load_block_entry(
store: &dyn Database,
config: &Config,
block_hash: &Hash,
) -> SubsystemResult<Option<BlockEntry>> {
load_decode(store, config.col_approval_data, &block_entry_key(block_hash))
.map(|u: Option<BlockEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a candidate entry from the aux store in current version format.
pub fn load_candidate_entry(
store: &dyn Database,
config: &Config,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>> {
load_decode(store, config.col_approval_data, &candidate_entry_key(candidate_hash))
.map(|u: Option<CandidateEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a candidate entry from the aux store in v1 format.
pub fn load_candidate_entry_v1(
store: &dyn Database,
config: &Config,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<super::v1::CandidateEntry>> {
load_decode(store, config.col_approval_data, &candidate_entry_key(candidate_hash))
.map(|u: Option<super::v1::CandidateEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
/// Load a block entry from the aux store in v1 format.
pub fn load_block_entry_v1(
store: &dyn Database,
config: &Config,
block_hash: &Hash,
) -> SubsystemResult<Option<super::v1::BlockEntry>> {
load_decode(store, config.col_approval_data, &block_entry_key(block_hash))
.map(|u: Option<super::v1::BlockEntry>| u.map(|v| v.into()))
.map_err(|e| SubsystemError::with_origin("approval-voting", e))
}
@@ -0,0 +1,570 @@
// Copyright (C) Parity Technologies (UK) Ltd.
// This file is part of Polkadot.
// Polkadot is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Polkadot is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
//! Tests for the aux-schema of approval voting.
use super::{DbBackend, StoredBlockRange, *};
use crate::{
backend::{Backend, OverlayedBackend},
ops::{add_block_entry, canonicalize, force_approve, NewCandidateInfo},
};
use polkadot_node_subsystem_util::database::Database;
use polkadot_primitives::Id as ParaId;
use std::{collections::HashMap, sync::Arc};
use ::test_helpers::{dummy_candidate_receipt, dummy_candidate_receipt_bad_sig, dummy_hash};
const DATA_COL: u32 = 0;
const NUM_COLUMNS: u32 = 1;
const TEST_CONFIG: Config = Config { col_approval_data: DATA_COL };
fn make_db() -> (DbBackend, Arc<dyn Database>) {
let db = kvdb_memorydb::create(NUM_COLUMNS);
let db = polkadot_node_subsystem_util::database::kvdb_impl::DbAdapter::new(db, &[]);
let db_writer: Arc<dyn Database> = Arc::new(db);
(DbBackend::new(db_writer.clone(), TEST_CONFIG), db_writer)
}
fn make_block_entry(
block_hash: Hash,
parent_hash: Hash,
block_number: BlockNumber,
candidates: Vec<(CoreIndex, CandidateHash)>,
) -> BlockEntry {
BlockEntry {
block_hash,
parent_hash,
block_number,
session: 1,
slot: Slot::from(1),
relay_vrf_story: [0u8; 32],
approved_bitfield: make_bitvec(candidates.len()),
candidates,
children: Vec::new(),
distributed_assignments: Default::default(),
}
}
fn make_bitvec(len: usize) -> BitVec<u8, BitOrderLsb0> {
bitvec::bitvec![u8, BitOrderLsb0; 0; len]
}
fn make_candidate(para_id: ParaId, relay_parent: Hash) -> CandidateReceipt {
let mut c = dummy_candidate_receipt(dummy_hash());
c.descriptor.para_id = para_id;
c.descriptor.relay_parent = relay_parent;
c
}
#[test]
fn read_write() {
let (mut db, store) = make_db();
let hash_a = Hash::repeat_byte(1);
let hash_b = Hash::repeat_byte(2);
let candidate_hash = dummy_candidate_receipt_bad_sig(dummy_hash(), None).hash();
let range = StoredBlockRange(10, 20);
let at_height = vec![hash_a, hash_b];
let block_entry =
make_block_entry(hash_a, Default::default(), 1, vec![(CoreIndex(0), candidate_hash)]);
let candidate_entry = CandidateEntry {
candidate: dummy_candidate_receipt_bad_sig(dummy_hash(), None),
session: 5,
block_assignments: vec![(
hash_a,
ApprovalEntry {
tranches: Vec::new(),
backing_group: GroupIndex(1),
our_assignment: None,
our_approval_sig: None,
assigned_validators: Default::default(),
approved: false,
},
)]
.into_iter()
.collect(),
approvals: Default::default(),
};
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(range.clone());
overlay_db.write_blocks_at_height(1, at_height.clone());
overlay_db.write_block_entry(block_entry.clone().into());
overlay_db.write_candidate_entry(candidate_entry.clone().into());
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(load_stored_blocks(store.as_ref(), &TEST_CONFIG).unwrap(), Some(range));
assert_eq!(load_blocks_at_height(store.as_ref(), &TEST_CONFIG, &1).unwrap(), at_height);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap(),
Some(block_entry.into())
);
assert_eq!(
load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash).unwrap(),
Some(candidate_entry.into()),
);
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.delete_blocks_at_height(1);
overlay_db.delete_block_entry(&hash_a);
overlay_db.delete_candidate_entry(&candidate_hash);
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert!(load_blocks_at_height(store.as_ref(), &TEST_CONFIG, &1).unwrap().is_empty());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash_a).unwrap().is_none());
assert!(load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash)
.unwrap()
.is_none());
}
#[test]
fn add_block_entry_works() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let candidate_receipt_a = make_candidate(ParaId::from(1_u32), parent_hash);
let candidate_receipt_b = make_candidate(ParaId::from(2_u32), parent_hash);
let candidate_hash_a = candidate_receipt_a.hash();
let candidate_hash_b = candidate_receipt_b.hash();
let block_number = 10;
let block_entry_a = make_block_entry(
block_hash_a,
parent_hash,
block_number,
vec![(CoreIndex(0), candidate_hash_a)],
);
let block_entry_b = make_block_entry(
block_hash_b,
parent_hash,
block_number,
vec![(CoreIndex(0), candidate_hash_a), (CoreIndex(1), candidate_hash_b)],
);
let n_validators = 10;
let mut new_candidate_info = HashMap::new();
new_candidate_info
.insert(candidate_hash_a, NewCandidateInfo::new(candidate_receipt_a, GroupIndex(0), None));
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |h| {
new_candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
new_candidate_info
.insert(candidate_hash_b, NewCandidateInfo::new(candidate_receipt_b, GroupIndex(1), None));
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |h| {
new_candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
Some(block_entry_a.into())
);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
Some(block_entry_b.into())
);
let candidate_entry_a = load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash_a)
.unwrap()
.unwrap();
assert_eq!(
candidate_entry_a.block_assignments.keys().collect::<Vec<_>>(),
vec![&block_hash_a, &block_hash_b]
);
let candidate_entry_b = load_candidate_entry(store.as_ref(), &TEST_CONFIG, &candidate_hash_b)
.unwrap()
.unwrap();
assert_eq!(candidate_entry_b.block_assignments.keys().collect::<Vec<_>>(), vec![&block_hash_b]);
}
#[test]
fn add_block_entry_adds_child() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let mut block_entry_a = make_block_entry(block_hash_a, parent_hash, 1, Vec::new());
let block_entry_b = make_block_entry(block_hash_b, block_hash_a, 2, Vec::new());
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |_| None).unwrap();
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |_| None).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
block_entry_a.children.push(block_hash_b);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a).unwrap(),
Some(block_entry_a.into())
);
assert_eq!(
load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b).unwrap(),
Some(block_entry_b.into())
);
}
#[test]
fn canonicalize_works() {
let (mut db, store) = make_db();
// -> B1 -> C1 -> D1
// A -> B2 -> C2 -> D2
//
// We'll canonicalize C1. Everytning except D1 should disappear.
//
// Candidates:
// Cand1 in B2
// Cand2 in C2
// Cand3 in C2 and D1
// Cand4 in D1
// Cand5 in D2
// Only Cand3 and Cand4 should remain after canonicalize.
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(StoredBlockRange(1, 5));
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let genesis = Hash::repeat_byte(0);
let block_hash_a = Hash::repeat_byte(1);
let block_hash_b1 = Hash::repeat_byte(2);
let block_hash_b2 = Hash::repeat_byte(3);
let block_hash_c1 = Hash::repeat_byte(4);
let block_hash_c2 = Hash::repeat_byte(5);
let block_hash_d1 = Hash::repeat_byte(6);
let block_hash_d2 = Hash::repeat_byte(7);
let candidate_receipt_genesis = make_candidate(ParaId::from(1_u32), genesis);
let candidate_receipt_a = make_candidate(ParaId::from(2_u32), block_hash_a);
let candidate_receipt_b = make_candidate(ParaId::from(3_u32), block_hash_a);
let candidate_receipt_b1 = make_candidate(ParaId::from(4_u32), block_hash_b1);
let candidate_receipt_c1 = make_candidate(ParaId::from(5_u32), block_hash_c1);
let cand_hash_1 = candidate_receipt_genesis.hash();
let cand_hash_2 = candidate_receipt_a.hash();
let cand_hash_3 = candidate_receipt_b.hash();
let cand_hash_4 = candidate_receipt_b1.hash();
let cand_hash_5 = candidate_receipt_c1.hash();
let block_entry_a = make_block_entry(block_hash_a, genesis, 1, Vec::new());
let block_entry_b1 = make_block_entry(block_hash_b1, block_hash_a, 2, Vec::new());
let block_entry_b2 =
make_block_entry(block_hash_b2, block_hash_a, 2, vec![(CoreIndex(0), cand_hash_1)]);
let block_entry_c1 = make_block_entry(block_hash_c1, block_hash_b1, 3, Vec::new());
let block_entry_c2 = make_block_entry(
block_hash_c2,
block_hash_b2,
3,
vec![(CoreIndex(0), cand_hash_2), (CoreIndex(1), cand_hash_3)],
);
let block_entry_d1 = make_block_entry(
block_hash_d1,
block_hash_c1,
4,
vec![(CoreIndex(0), cand_hash_3), (CoreIndex(1), cand_hash_4)],
);
let block_entry_d2 =
make_block_entry(block_hash_d2, block_hash_c2, 4, vec![(CoreIndex(0), cand_hash_5)]);
let candidate_info = {
let mut candidate_info = HashMap::new();
candidate_info.insert(
cand_hash_1,
NewCandidateInfo::new(candidate_receipt_genesis, GroupIndex(1), None),
);
candidate_info
.insert(cand_hash_2, NewCandidateInfo::new(candidate_receipt_a, GroupIndex(2), None));
candidate_info
.insert(cand_hash_3, NewCandidateInfo::new(candidate_receipt_b, GroupIndex(3), None));
candidate_info
.insert(cand_hash_4, NewCandidateInfo::new(candidate_receipt_b1, GroupIndex(4), None));
candidate_info
.insert(cand_hash_5, NewCandidateInfo::new(candidate_receipt_c1, GroupIndex(5), None));
candidate_info
};
// now insert all the blocks.
let blocks = vec![
block_entry_a.clone(),
block_entry_b1.clone(),
block_entry_b2.clone(),
block_entry_c1.clone(),
block_entry_c2.clone(),
block_entry_d1.clone(),
block_entry_d2.clone(),
];
let mut overlay_db = OverlayedBackend::new(&db);
for block_entry in blocks {
add_block_entry(&mut overlay_db, block_entry.into(), n_validators, |h| {
candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
}
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let check_candidates_in_store = |expected: Vec<(CandidateHash, Option<Vec<_>>)>| {
for (c_hash, in_blocks) in expected {
let (entry, in_blocks) = match in_blocks {
None => {
assert!(load_candidate_entry(store.as_ref(), &TEST_CONFIG, &c_hash)
.unwrap()
.is_none());
continue
},
Some(i) => (
load_candidate_entry(store.as_ref(), &TEST_CONFIG, &c_hash).unwrap().unwrap(),
i,
),
};
assert_eq!(entry.block_assignments.len(), in_blocks.len());
for x in in_blocks {
assert!(entry.block_assignments.contains_key(&x));
}
}
};
let check_blocks_in_store = |expected: Vec<(Hash, Option<Vec<_>>)>| {
for (hash, with_candidates) in expected {
let (entry, with_candidates) = match with_candidates {
None => {
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash)
.unwrap()
.is_none());
continue
},
Some(i) =>
(load_block_entry(store.as_ref(), &TEST_CONFIG, &hash).unwrap().unwrap(), i),
};
assert_eq!(entry.candidates.len(), with_candidates.len());
for x in with_candidates {
assert!(entry.candidates.iter().any(|(_, c)| c == &x));
}
}
};
check_candidates_in_store(vec![
(cand_hash_1, Some(vec![block_hash_b2])),
(cand_hash_2, Some(vec![block_hash_c2])),
(cand_hash_3, Some(vec![block_hash_c2, block_hash_d1])),
(cand_hash_4, Some(vec![block_hash_d1])),
(cand_hash_5, Some(vec![block_hash_d2])),
]);
check_blocks_in_store(vec![
(block_hash_a, Some(vec![])),
(block_hash_b1, Some(vec![])),
(block_hash_b2, Some(vec![cand_hash_1])),
(block_hash_c1, Some(vec![])),
(block_hash_c2, Some(vec![cand_hash_2, cand_hash_3])),
(block_hash_d1, Some(vec![cand_hash_3, cand_hash_4])),
(block_hash_d2, Some(vec![cand_hash_5])),
]);
let mut overlay_db = OverlayedBackend::new(&db);
canonicalize(&mut overlay_db, 3, block_hash_c1).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_stored_blocks(store.as_ref(), &TEST_CONFIG).unwrap().unwrap(),
StoredBlockRange(4, 5)
);
check_candidates_in_store(vec![
(cand_hash_1, None),
(cand_hash_2, None),
(cand_hash_3, Some(vec![block_hash_d1])),
(cand_hash_4, Some(vec![block_hash_d1])),
(cand_hash_5, None),
]);
check_blocks_in_store(vec![
(block_hash_a, None),
(block_hash_b1, None),
(block_hash_b2, None),
(block_hash_c1, None),
(block_hash_c2, None),
(block_hash_d1, Some(vec![cand_hash_3, cand_hash_4])),
(block_hash_d2, None),
]);
}
#[test]
fn force_approve_works() {
let (mut db, store) = make_db();
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
overlay_db.write_stored_block_range(StoredBlockRange(1, 4));
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
let candidate_hash = CandidateHash(Hash::repeat_byte(42));
let single_candidate_vec = vec![(CoreIndex(0), candidate_hash)];
let candidate_info = {
let mut candidate_info = HashMap::new();
candidate_info.insert(
candidate_hash,
NewCandidateInfo::new(
make_candidate(ParaId::from(1_u32), Default::default()),
GroupIndex(1),
None,
),
);
candidate_info
};
let block_hash_a = Hash::repeat_byte(1); // 1
let block_hash_b = Hash::repeat_byte(2);
let block_hash_c = Hash::repeat_byte(3);
let block_hash_d = Hash::repeat_byte(4); // 4
let block_entry_a =
make_block_entry(block_hash_a, Default::default(), 1, single_candidate_vec.clone());
let block_entry_b =
make_block_entry(block_hash_b, block_hash_a, 2, single_candidate_vec.clone());
let block_entry_c =
make_block_entry(block_hash_c, block_hash_b, 3, single_candidate_vec.clone());
let block_entry_d =
make_block_entry(block_hash_d, block_hash_c, 4, single_candidate_vec.clone());
let blocks = vec![
block_entry_a.clone(),
block_entry_b.clone(),
block_entry_c.clone(),
block_entry_d.clone(),
];
let mut overlay_db = OverlayedBackend::new(&db);
for block_entry in blocks {
add_block_entry(&mut overlay_db, block_entry.into(), n_validators, |h| {
candidate_info.get(h).map(|x| x.clone())
})
.unwrap();
}
let approved_hashes = force_approve(&mut overlay_db, block_hash_d, 2).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_a,)
.unwrap()
.unwrap()
.approved_bitfield
.all());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_b,)
.unwrap()
.unwrap()
.approved_bitfield
.all());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_c,)
.unwrap()
.unwrap()
.approved_bitfield
.not_any());
assert!(load_block_entry(store.as_ref(), &TEST_CONFIG, &block_hash_d,)
.unwrap()
.unwrap()
.approved_bitfield
.not_any());
assert_eq!(approved_hashes, vec![block_hash_b, block_hash_a]);
}
#[test]
fn load_all_blocks_works() {
let (mut db, store) = make_db();
let parent_hash = Hash::repeat_byte(1);
let block_hash_a = Hash::repeat_byte(2);
let block_hash_b = Hash::repeat_byte(69);
let block_hash_c = Hash::repeat_byte(42);
let block_number = 10;
let block_entry_a = make_block_entry(block_hash_a, parent_hash, block_number, vec![]);
let block_entry_b = make_block_entry(block_hash_b, parent_hash, block_number, vec![]);
let block_entry_c = make_block_entry(block_hash_c, block_hash_a, block_number + 1, vec![]);
let n_validators = 10;
let mut overlay_db = OverlayedBackend::new(&db);
add_block_entry(&mut overlay_db, block_entry_a.clone().into(), n_validators, |_| None).unwrap();
// add C before B to test sorting.
add_block_entry(&mut overlay_db, block_entry_c.clone().into(), n_validators, |_| None).unwrap();
add_block_entry(&mut overlay_db, block_entry_b.clone().into(), n_validators, |_| None).unwrap();
let write_ops = overlay_db.into_write_ops();
db.write(write_ops).unwrap();
assert_eq!(
load_all_blocks(store.as_ref(), &TEST_CONFIG).unwrap(),
vec![block_hash_a, block_hash_b, block_hash_c],
)
}
@@ -27,7 +27,7 @@ use polkadot_primitives::{BlockNumber, CandidateHash, Hash};
use std::collections::HashMap;
use super::{
approval_db::v1::StoredBlockRange,
approval_db::v2::StoredBlockRange,
persisted_entries::{BlockEntry, CandidateEntry},
};
@@ -44,6 +44,7 @@ pub enum BackendWriteOp {
}
/// An abstraction over backend storage for the logic of this subsystem.
/// Implementation must always target latest storage version.
pub trait Backend {
/// Load a block entry from the DB.
fn load_block_entry(&self, hash: &Hash) -> SubsystemResult<Option<BlockEntry>>;
@@ -52,6 +53,7 @@ pub trait Backend {
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>>;
/// Load all blocks at a specific height.
fn load_blocks_at_height(&self, height: &BlockNumber) -> SubsystemResult<Vec<Hash>>;
/// Load all block from the DB.
@@ -64,6 +66,18 @@ pub trait Backend {
I: IntoIterator<Item = BackendWriteOp>;
}
/// A read only backend to enable db migration from version 1 of DB.
pub trait V1ReadBackend: Backend {
/// Load a candidate entry from the DB with scheme version 1.
fn load_candidate_entry_v1(
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>>;
/// Load a block entry from the DB with scheme version 1.
fn load_block_entry_v1(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>>;
}
// Status of block range in the `OverlayedBackend`.
#[derive(PartialEq)]
enum BlockRangeStatus {
@@ -16,28 +16,36 @@
//! Assignment criteria VRF generation and checking.
use itertools::Itertools;
use parity_scale_codec::{Decode, Encode};
use polkadot_node_primitives::approval::{
self as approval_types, AssignmentCert, AssignmentCertKind, DelayTranche, RelayVRFStory,
self as approval_types,
v1::{AssignmentCert, AssignmentCertKind, DelayTranche, RelayVRFStory},
v2::{AssignmentCertKindV2, AssignmentCertV2, CoreBitfield, VrfOutput, VrfProof, VrfSignature},
};
use polkadot_primitives::{
AssignmentId, AssignmentPair, CandidateHash, CoreIndex, GroupIndex, IndexedVec, SessionInfo,
ValidatorIndex,
};
use rand::{seq::SliceRandom, SeedableRng};
use rand_chacha::ChaCha20Rng;
use sc_keystore::LocalKeystore;
use sp_application_crypto::ByteArray;
use merlin::Transcript;
use schnorrkel::vrf::VRFInOut;
use std::collections::{hash_map::Entry, HashMap};
use std::{
cmp::min,
collections::{hash_map::Entry, HashMap},
};
use super::LOG_TARGET;
/// Details pertaining to our assignment on a block.
#[derive(Debug, Clone, Encode, Decode, PartialEq)]
pub struct OurAssignment {
cert: AssignmentCert,
cert: AssignmentCertV2,
tranche: DelayTranche,
validator_index: ValidatorIndex,
// Whether the assignment has been triggered already.
@@ -45,7 +53,7 @@ pub struct OurAssignment {
}
impl OurAssignment {
pub(crate) fn cert(&self) -> &AssignmentCert {
pub(crate) fn cert(&self) -> &AssignmentCertV2 {
&self.cert
}
@@ -66,8 +74,8 @@ impl OurAssignment {
}
}
impl From<crate::approval_db::v1::OurAssignment> for OurAssignment {
fn from(entry: crate::approval_db::v1::OurAssignment) -> Self {
impl From<crate::approval_db::v2::OurAssignment> for OurAssignment {
fn from(entry: crate::approval_db::v2::OurAssignment) -> Self {
OurAssignment {
cert: entry.cert,
tranche: entry.tranche,
@@ -77,7 +85,7 @@ impl From<crate::approval_db::v1::OurAssignment> for OurAssignment {
}
}
impl From<OurAssignment> for crate::approval_db::v1::OurAssignment {
impl From<OurAssignment> for crate::approval_db::v2::OurAssignment {
fn from(entry: OurAssignment) -> Self {
Self {
cert: entry.cert,
@@ -88,17 +96,97 @@ impl From<OurAssignment> for crate::approval_db::v1::OurAssignment {
}
}
fn relay_vrf_modulo_transcript(relay_vrf_story: RelayVRFStory, sample: u32) -> Transcript {
// combine the relay VRF story with a sample number.
let mut t = Transcript::new(approval_types::RELAY_VRF_MODULO_CONTEXT);
t.append_message(b"RC-VRF", &relay_vrf_story.0);
sample.using_encoded(|s| t.append_message(b"sample", s));
// Combines the relay VRF story with a sample number if any.
fn relay_vrf_modulo_transcript_inner(
mut transcript: Transcript,
relay_vrf_story: RelayVRFStory,
sample: Option<u32>,
) -> Transcript {
transcript.append_message(b"RC-VRF", &relay_vrf_story.0);
t
if let Some(sample) = sample {
sample.using_encoded(|s| transcript.append_message(b"sample", s));
}
transcript
}
fn relay_vrf_modulo_transcript_v1(relay_vrf_story: RelayVRFStory, sample: u32) -> Transcript {
relay_vrf_modulo_transcript_inner(
Transcript::new(approval_types::v1::RELAY_VRF_MODULO_CONTEXT),
relay_vrf_story,
Some(sample),
)
}
fn relay_vrf_modulo_transcript_v2(relay_vrf_story: RelayVRFStory) -> Transcript {
relay_vrf_modulo_transcript_inner(
Transcript::new(approval_types::v2::RELAY_VRF_MODULO_CONTEXT),
relay_vrf_story,
None,
)
}
/// A hard upper bound on num_cores * target_checkers / num_validators
const MAX_MODULO_SAMPLES: usize = 40;
/// Takes the VRF output as input and returns a Vec of cores the validator is assigned
/// to as a tranche0 checker.
fn relay_vrf_modulo_cores(
vrf_in_out: &VRFInOut,
// Configuration - `relay_vrf_modulo_samples`.
num_samples: u32,
// Configuration - `n_cores`.
max_cores: u32,
) -> Vec<CoreIndex> {
let rand_chacha =
ChaCha20Rng::from_seed(vrf_in_out.make_bytes::<<ChaCha20Rng as SeedableRng>::Seed>(
approval_types::v2::CORE_RANDOMNESS_CONTEXT,
));
generate_samples(rand_chacha, num_samples as usize, max_cores as usize)
}
/// Generates `num_sumples` randomly from (0..max_cores) range
///
/// Note! The algorithm can't change because validators on the other
/// side won't be able to check the assignments until they update.
/// This invariant is tested with `generate_samples_invariant`, so the
/// tests will catch any subtle changes in the implementation of this function
/// and its dependencies.
fn generate_samples(
mut rand_chacha: ChaCha20Rng,
num_samples: usize,
max_cores: usize,
) -> Vec<CoreIndex> {
if num_samples as usize > MAX_MODULO_SAMPLES {
gum::warn!(
target: LOG_TARGET,
n_cores = max_cores,
num_samples,
max_modulo_samples = MAX_MODULO_SAMPLES,
"`num_samples` is greater than `MAX_MODULO_SAMPLES`",
);
}
if 2 * num_samples > max_cores {
gum::debug!(
target: LOG_TARGET,
n_cores = max_cores,
num_samples,
max_modulo_samples = MAX_MODULO_SAMPLES,
"Suboptimal configuration `num_samples` should be less than `n_cores` / 2",
);
}
let num_samples = min(MAX_MODULO_SAMPLES, min(num_samples, max_cores));
let mut random_cores = (0..max_cores as u32).map(|val| val.into()).collect::<Vec<CoreIndex>>();
let (samples, _) = random_cores.partial_shuffle(&mut rand_chacha, num_samples as usize);
samples.into_iter().map(|val| *val).collect_vec()
}
fn relay_vrf_modulo_core(vrf_in_out: &VRFInOut, n_cores: u32) -> CoreIndex {
let bytes: [u8; 4] = vrf_in_out.make_bytes(approval_types::CORE_RANDOMNESS_CONTEXT);
let bytes: [u8; 4] = vrf_in_out.make_bytes(approval_types::v1::CORE_RANDOMNESS_CONTEXT);
// interpret as little-endian u32.
let random_core = u32::from_le_bytes(bytes) % n_cores;
@@ -106,7 +194,7 @@ fn relay_vrf_modulo_core(vrf_in_out: &VRFInOut, n_cores: u32) -> CoreIndex {
}
fn relay_vrf_delay_transcript(relay_vrf_story: RelayVRFStory, core_index: CoreIndex) -> Transcript {
let mut t = Transcript::new(approval_types::RELAY_VRF_DELAY_CONTEXT);
let mut t = Transcript::new(approval_types::v1::RELAY_VRF_DELAY_CONTEXT);
t.append_message(b"RC-VRF", &relay_vrf_story.0);
core_index.0.using_encoded(|s| t.append_message(b"core", s));
t
@@ -117,7 +205,7 @@ fn relay_vrf_delay_tranche(
num_delay_tranches: u32,
zeroth_delay_tranche_width: u32,
) -> DelayTranche {
let bytes: [u8; 4] = vrf_in_out.make_bytes(approval_types::TRANCHE_RANDOMNESS_CONTEXT);
let bytes: [u8; 4] = vrf_in_out.make_bytes(approval_types::v1::TRANCHE_RANDOMNESS_CONTEXT);
// interpret as little-endian u32 and reduce by the number of tranches.
let wide_tranche =
@@ -128,13 +216,13 @@ fn relay_vrf_delay_tranche(
}
fn assigned_core_transcript(core_index: CoreIndex) -> Transcript {
let mut t = Transcript::new(approval_types::ASSIGNED_CORE_CONTEXT);
let mut t = Transcript::new(approval_types::v1::ASSIGNED_CORE_CONTEXT);
core_index.0.using_encoded(|s| t.append_message(b"core", s));
t
}
/// Information about the world assignments are being produced in.
#[derive(Clone)]
#[derive(Clone, Debug)]
pub(crate) struct Config {
/// The assignment public keys for validators.
assignment_keys: Vec<AssignmentId>,
@@ -175,12 +263,13 @@ pub(crate) trait AssignmentCriteria {
fn check_assignment_cert(
&self,
claimed_core_index: CoreIndex,
claimed_core_bitfield: CoreBitfield,
validator_index: ValidatorIndex,
config: &Config,
relay_vrf_story: RelayVRFStory,
assignment: &AssignmentCert,
backing_group: GroupIndex,
assignment: &AssignmentCertV2,
// Backing groups for each "leaving core".
backing_groups: Vec<GroupIndex>,
) -> Result<DelayTranche, InvalidAssignment>;
}
@@ -194,25 +283,25 @@ impl AssignmentCriteria for RealAssignmentCriteria {
config: &Config,
leaving_cores: Vec<(CandidateHash, CoreIndex, GroupIndex)>,
) -> HashMap<CoreIndex, OurAssignment> {
compute_assignments(keystore, relay_vrf_story, config, leaving_cores)
compute_assignments(keystore, relay_vrf_story, config, leaving_cores, false)
}
fn check_assignment_cert(
&self,
claimed_core_index: CoreIndex,
claimed_core_bitfield: CoreBitfield,
validator_index: ValidatorIndex,
config: &Config,
relay_vrf_story: RelayVRFStory,
assignment: &AssignmentCert,
backing_group: GroupIndex,
assignment: &AssignmentCertV2,
backing_groups: Vec<GroupIndex>,
) -> Result<DelayTranche, InvalidAssignment> {
check_assignment_cert(
claimed_core_index,
claimed_core_bitfield,
validator_index,
config,
relay_vrf_story,
assignment,
backing_group,
backing_groups,
)
}
}
@@ -233,6 +322,7 @@ pub(crate) fn compute_assignments(
relay_vrf_story: RelayVRFStory,
config: &Config,
leaving_cores: impl IntoIterator<Item = (CandidateHash, CoreIndex, GroupIndex)> + Clone,
enable_v2_assignments: bool,
) -> HashMap<CoreIndex, OurAssignment> {
if config.n_cores == 0 ||
config.assignment_keys.is_empty() ||
@@ -291,14 +381,25 @@ pub(crate) fn compute_assignments(
let mut assignments = HashMap::new();
// First run `RelayVRFModulo` for each sample.
compute_relay_vrf_modulo_assignments(
&assignments_key,
index,
config,
relay_vrf_story.clone(),
leaving_cores.iter().cloned(),
&mut assignments,
);
if enable_v2_assignments {
compute_relay_vrf_modulo_assignments_v2(
&assignments_key,
index,
config,
relay_vrf_story.clone(),
leaving_cores.clone(),
&mut assignments,
);
} else {
compute_relay_vrf_modulo_assignments_v1(
&assignments_key,
index,
config,
relay_vrf_story.clone(),
leaving_cores.clone(),
&mut assignments,
);
}
// Then run `RelayVRFDelay` once for the whole block.
compute_relay_vrf_delay_assignments(
@@ -313,7 +414,7 @@ pub(crate) fn compute_assignments(
assignments
}
fn compute_relay_vrf_modulo_assignments(
fn compute_relay_vrf_modulo_assignments_v1(
assignments_key: &schnorrkel::Keypair,
validator_index: ValidatorIndex,
config: &Config,
@@ -329,7 +430,7 @@ fn compute_relay_vrf_modulo_assignments(
// into closure.
let core = &mut core;
assignments_key.vrf_sign_extra_after_check(
relay_vrf_modulo_transcript(relay_vrf_story.clone(), rvm_sample),
relay_vrf_modulo_transcript_v1(relay_vrf_story.clone(), rvm_sample),
|vrf_in_out| {
*core = relay_vrf_modulo_core(&vrf_in_out, config.n_cores);
if let Some((candidate_hash, _)) =
@@ -357,15 +458,15 @@ fn compute_relay_vrf_modulo_assignments(
// has been executed.
let cert = AssignmentCert {
kind: AssignmentCertKind::RelayVRFModulo { sample: rvm_sample },
vrf: approval_types::VrfSignature {
output: approval_types::VrfOutput(vrf_in_out.to_output()),
proof: approval_types::VrfProof(vrf_proof),
vrf: VrfSignature {
output: VrfOutput(vrf_in_out.to_output()),
proof: VrfProof(vrf_proof),
},
};
// All assignments of type RelayVRFModulo have tranche 0.
assignments.entry(core).or_insert(OurAssignment {
cert,
cert: cert.into(),
tranche: 0,
validator_index,
triggered: false,
@@ -374,6 +475,84 @@ fn compute_relay_vrf_modulo_assignments(
}
}
fn assigned_cores_transcript(core_bitfield: &CoreBitfield) -> Transcript {
let mut t = Transcript::new(approval_types::v2::ASSIGNED_CORE_CONTEXT);
core_bitfield.using_encoded(|s| t.append_message(b"cores", s));
t
}
fn compute_relay_vrf_modulo_assignments_v2(
assignments_key: &schnorrkel::Keypair,
validator_index: ValidatorIndex,
config: &Config,
relay_vrf_story: RelayVRFStory,
leaving_cores: Vec<(CandidateHash, CoreIndex)>,
assignments: &mut HashMap<CoreIndex, OurAssignment>,
) {
let mut assigned_cores = Vec::new();
let leaving_cores = leaving_cores.iter().map(|(_, core)| core).collect::<Vec<_>>();
let maybe_assignment = {
let assigned_cores = &mut assigned_cores;
assignments_key.vrf_sign_extra_after_check(
relay_vrf_modulo_transcript_v2(relay_vrf_story.clone()),
|vrf_in_out| {
*assigned_cores = relay_vrf_modulo_cores(
&vrf_in_out,
config.relay_vrf_modulo_samples,
config.n_cores,
)
.into_iter()
.filter(|core| leaving_cores.contains(&core))
.collect::<Vec<CoreIndex>>();
if !assigned_cores.is_empty() {
gum::trace!(
target: LOG_TARGET,
?assigned_cores,
?validator_index,
tranche = 0,
"RelayVRFModuloCompact Assignment."
);
let assignment_bitfield: CoreBitfield = assigned_cores
.clone()
.try_into()
.expect("Just checked `!assigned_cores.is_empty()`; qed");
Some(assigned_cores_transcript(&assignment_bitfield))
} else {
None
}
},
)
};
if let Some(assignment) = maybe_assignment.map(|(vrf_in_out, vrf_proof, _)| {
let assignment_bitfield: CoreBitfield = assigned_cores
.clone()
.try_into()
.expect("Just checked `!assigned_cores.is_empty()`; qed");
let cert = AssignmentCertV2 {
kind: AssignmentCertKindV2::RelayVRFModuloCompact {
core_bitfield: assignment_bitfield.clone(),
},
vrf: VrfSignature {
output: VrfOutput(vrf_in_out.to_output()),
proof: VrfProof(vrf_proof),
},
};
// All assignments of type RelayVRFModulo have tranche 0.
OurAssignment { cert, tranche: 0, validator_index, triggered: false }
}) {
for core_index in assigned_cores {
assignments.insert(core_index, assignment.clone());
}
}
}
fn compute_relay_vrf_delay_assignments(
assignments_key: &schnorrkel::Keypair,
validator_index: ValidatorIndex,
@@ -392,11 +571,11 @@ fn compute_relay_vrf_delay_assignments(
config.zeroth_delay_tranche_width,
);
let cert = AssignmentCert {
kind: AssignmentCertKind::RelayVRFDelay { core_index: core },
vrf: approval_types::VrfSignature {
output: approval_types::VrfOutput(vrf_in_out.to_output()),
proof: approval_types::VrfProof(vrf_proof),
let cert = AssignmentCertV2 {
kind: AssignmentCertKindV2::RelayVRFDelay { core_index: core },
vrf: VrfSignature {
output: VrfOutput(vrf_in_out.to_output()),
proof: VrfProof(vrf_proof),
},
};
@@ -453,12 +632,15 @@ pub(crate) enum InvalidAssignmentReason {
VRFModuloOutputMismatch,
VRFDelayCoreIndexMismatch,
VRFDelayOutputMismatch,
InvalidArguments,
/// Assignment vrf check resulted in 0 assigned cores.
NullAssignment,
}
/// Checks the crypto of an assignment cert. Failure conditions:
/// * Validator index out of bounds
/// * VRF signature check fails
/// * VRF output doesn't match assigned core
/// * VRF output doesn't match assigned cores
/// * Core is not covered by extra data in signature
/// * Core index out of bounds
/// * Sample is out of bounds
@@ -467,12 +649,12 @@ pub(crate) enum InvalidAssignmentReason {
/// This function does not check whether the core is actually a valid assignment or not. That should
/// be done outside the scope of this function.
pub(crate) fn check_assignment_cert(
claimed_core_index: CoreIndex,
claimed_core_indices: CoreBitfield,
validator_index: ValidatorIndex,
config: &Config,
relay_vrf_story: RelayVRFStory,
assignment: &AssignmentCert,
backing_group: GroupIndex,
assignment: &AssignmentCertV2,
backing_groups: Vec<GroupIndex>,
) -> Result<DelayTranche, InvalidAssignment> {
use InvalidAssignmentReason as Reason;
@@ -484,52 +666,133 @@ pub(crate) fn check_assignment_cert(
let public = schnorrkel::PublicKey::from_bytes(validator_public.as_slice())
.map_err(|_| InvalidAssignment(Reason::InvalidAssignmentKey))?;
if claimed_core_index.0 >= config.n_cores {
return Err(InvalidAssignment(Reason::CoreIndexOutOfBounds))
// Check that we have all backing groups for claimed cores.
if claimed_core_indices.count_ones() == 0 ||
claimed_core_indices.count_ones() != backing_groups.len()
{
return Err(InvalidAssignment(Reason::InvalidArguments))
}
// Check that the validator was not part of the backing group
// and not already assigned.
let is_in_backing =
is_in_backing_group(&config.validator_groups, validator_index, backing_group);
for (claimed_core, backing_group) in claimed_core_indices.iter_ones().zip(backing_groups.iter())
{
if claimed_core >= config.n_cores as usize {
return Err(InvalidAssignment(Reason::CoreIndexOutOfBounds))
}
if is_in_backing {
return Err(InvalidAssignment(Reason::IsInBackingGroup))
let is_in_backing =
is_in_backing_group(&config.validator_groups, validator_index, *backing_group);
if is_in_backing {
return Err(InvalidAssignment(Reason::IsInBackingGroup))
}
}
let vrf_signature = &assignment.vrf;
match assignment.kind {
AssignmentCertKind::RelayVRFModulo { sample } => {
if sample >= config.relay_vrf_modulo_samples {
return Err(InvalidAssignment(Reason::SampleOutOfBounds))
let vrf_output = &assignment.vrf.output;
let vrf_proof = &assignment.vrf.proof;
let first_claimed_core_index =
claimed_core_indices.first_one().expect("Checked above; qed") as u32;
match &assignment.kind {
AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield } => {
// Check that claimed core bitfield match the one from certificate.
if &claimed_core_indices != core_bitfield {
return Err(InvalidAssignment(Reason::VRFModuloCoreIndexMismatch))
}
let (vrf_in_out, _) = public
.vrf_verify_extra(
relay_vrf_modulo_transcript(relay_vrf_story, sample),
&vrf_signature.output.0,
&vrf_signature.proof.0,
assigned_core_transcript(claimed_core_index),
relay_vrf_modulo_transcript_v2(relay_vrf_story),
&vrf_output.0,
&vrf_proof.0,
assigned_cores_transcript(core_bitfield),
)
.map_err(|_| InvalidAssignment(Reason::VRFModuloOutputMismatch))?;
let resulting_cores = relay_vrf_modulo_cores(
&vrf_in_out,
config.relay_vrf_modulo_samples,
config.n_cores,
);
// Currently validators can opt out of checking specific cores.
// This is the same issue to how validator can opt out and not send their assignments in
// the first place. Ensure that the `vrf_in_out` actually includes all of the claimed
// cores.
for claimed_core_index in claimed_core_indices.iter_ones() {
if !resulting_cores.contains(&CoreIndex(claimed_core_index as u32)) {
gum::debug!(
target: LOG_TARGET,
?resulting_cores,
?claimed_core_indices,
vrf_modulo_cores = ?resulting_cores,
"Assignment claimed cores mismatch",
);
return Err(InvalidAssignment(Reason::VRFModuloCoreIndexMismatch))
}
}
Ok(0)
},
AssignmentCertKindV2::RelayVRFModulo { sample } => {
if *sample >= config.relay_vrf_modulo_samples {
return Err(InvalidAssignment(Reason::SampleOutOfBounds))
}
// Enforce claimed candidates is 1.
if claimed_core_indices.count_ones() != 1 {
gum::warn!(
target: LOG_TARGET,
?claimed_core_indices,
"`RelayVRFModulo` assignment must always claim 1 core",
);
return Err(InvalidAssignment(Reason::InvalidArguments))
}
let (vrf_in_out, _) = public
.vrf_verify_extra(
relay_vrf_modulo_transcript_v1(relay_vrf_story, *sample),
&vrf_output.0,
&vrf_proof.0,
assigned_core_transcript(CoreIndex(first_claimed_core_index)),
)
.map_err(|_| InvalidAssignment(Reason::VRFModuloOutputMismatch))?;
let core = relay_vrf_modulo_core(&vrf_in_out, config.n_cores);
// ensure that the `vrf_in_out` actually gives us the claimed core.
if relay_vrf_modulo_core(&vrf_in_out, config.n_cores) == claimed_core_index {
if core.0 == first_claimed_core_index {
Ok(0)
} else {
gum::debug!(
target: LOG_TARGET,
?core,
?claimed_core_indices,
"Assignment claimed cores mismatch",
);
Err(InvalidAssignment(Reason::VRFModuloCoreIndexMismatch))
}
},
AssignmentCertKind::RelayVRFDelay { core_index } => {
if core_index != claimed_core_index {
AssignmentCertKindV2::RelayVRFDelay { core_index } => {
// Enforce claimed candidates is 1.
if claimed_core_indices.count_ones() != 1 {
gum::debug!(
target: LOG_TARGET,
?claimed_core_indices,
"`RelayVRFDelay` assignment must always claim 1 core",
);
return Err(InvalidAssignment(Reason::InvalidArguments))
}
if core_index.0 != first_claimed_core_index {
return Err(InvalidAssignment(Reason::VRFDelayCoreIndexMismatch))
}
let (vrf_in_out, _) = public
.vrf_verify(
relay_vrf_delay_transcript(relay_vrf_story, core_index),
&vrf_signature.output.0,
&vrf_signature.proof.0,
relay_vrf_delay_transcript(relay_vrf_story, *core_index),
&vrf_output.0,
&vrf_proof.0,
)
.map_err(|_| InvalidAssignment(Reason::VRFDelayOutputMismatch))?;
@@ -550,6 +813,19 @@ fn is_in_backing_group(
validator_groups.get(group).map_or(false, |g| g.contains(&validator))
}
/// Migration helpers.
impl From<crate::approval_db::v1::OurAssignment> for OurAssignment {
fn from(value: crate::approval_db::v1::OurAssignment) -> Self {
Self {
cert: value.cert.into(),
tranche: value.tranche,
validator_index: value.validator_index,
// Whether the assignment has been triggered already.
triggered: value.triggered,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -630,10 +906,11 @@ mod tests {
]),
n_cores: 2,
zeroth_delay_tranche_width: 10,
relay_vrf_modulo_samples: 3,
relay_vrf_modulo_samples: 10,
n_delay_tranches: 40,
},
vec![(c_a, CoreIndex(0), GroupIndex(1)), (c_b, CoreIndex(1), GroupIndex(0))],
false,
);
// Note that alice is in group 0, which was the backing group for core 1.
@@ -665,10 +942,11 @@ mod tests {
]),
n_cores: 2,
zeroth_delay_tranche_width: 10,
relay_vrf_modulo_samples: 3,
relay_vrf_modulo_samples: 10,
n_delay_tranches: 40,
},
vec![(c_a, CoreIndex(0), GroupIndex(0)), (c_b, CoreIndex(1), GroupIndex(1))],
false,
);
assert_eq!(assignments.len(), 1);
@@ -692,19 +970,21 @@ mod tests {
validator_groups: Default::default(),
n_cores: 0,
zeroth_delay_tranche_width: 10,
relay_vrf_modulo_samples: 3,
relay_vrf_modulo_samples: 10,
n_delay_tranches: 40,
},
vec![],
false,
);
assert!(assignments.is_empty());
}
#[derive(Debug)]
struct MutatedAssignment {
core: CoreIndex,
cert: AssignmentCert,
group: GroupIndex,
cores: CoreBitfield,
cert: AssignmentCertV2,
groups: Vec<GroupIndex>,
own_group: GroupIndex,
val_index: ValidatorIndex,
config: Config,
@@ -729,12 +1009,12 @@ mod tests {
validator_groups: basic_groups(n_validators, n_cores),
n_cores: n_cores as u32,
zeroth_delay_tranche_width: 10,
relay_vrf_modulo_samples: 3,
relay_vrf_modulo_samples: 15,
n_delay_tranches: 40,
};
let relay_vrf_story = RelayVRFStory([42u8; 32]);
let assignments = compute_assignments(
let mut assignments = compute_assignments(
&keystore,
relay_vrf_story.clone(),
&config,
@@ -747,19 +1027,42 @@ mod tests {
)
})
.collect::<Vec<_>>(),
false,
);
// Extend with v2 assignments as well
assignments.extend(compute_assignments(
&keystore,
relay_vrf_story.clone(),
&config,
(0..n_cores)
.map(|i| {
(
CandidateHash(Hash::repeat_byte(i as u8)),
CoreIndex(i as u32),
group_for_core(i),
)
})
.collect::<Vec<_>>(),
true,
));
let mut counted = 0;
for (core, assignment) in assignments {
let cores = match assignment.cert.kind.clone() {
AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield } => core_bitfield,
AssignmentCertKindV2::RelayVRFModulo { sample: _ } => core.into(),
AssignmentCertKindV2::RelayVRFDelay { core_index } => core_index.into(),
};
let mut mutated = MutatedAssignment {
core,
group: group_for_core(core.0 as _),
cores: cores.clone(),
groups: cores.iter_ones().map(|core| group_for_core(core)).collect(),
cert: assignment.cert,
own_group: GroupIndex(0),
val_index: ValidatorIndex(0),
config: config.clone(),
};
let expected = match f(&mut mutated) {
None => continue,
Some(e) => e,
@@ -768,16 +1071,16 @@ mod tests {
counted += 1;
let is_good = check_assignment_cert(
mutated.core,
mutated.cores,
mutated.val_index,
&mutated.config,
relay_vrf_story.clone(),
&mutated.cert,
mutated.group,
mutated.groups,
)
.is_ok();
assert_eq!(expected, is_good)
assert_eq!(expected, is_good);
}
assert!(counted > 0);
@@ -791,7 +1094,7 @@ mod tests {
#[test]
fn check_rejects_claimed_core_out_of_bounds() {
check_mutated_assignments(200, 100, 25, |m| {
m.core.0 += 100;
m.cores = CoreIndex(100).into();
Some(false)
});
}
@@ -799,7 +1102,7 @@ mod tests {
#[test]
fn check_rejects_in_backing_group() {
check_mutated_assignments(200, 100, 25, |m| {
m.group = m.own_group;
m.groups[0] = m.own_group;
Some(false)
});
}
@@ -814,10 +1117,11 @@ mod tests {
#[test]
fn check_rejects_delay_bad_vrf() {
check_mutated_assignments(40, 10, 8, |m| {
check_mutated_assignments(40, 100, 8, |m| {
let vrf_signature = garbage_vrf_signature();
match m.cert.kind.clone() {
AssignmentCertKind::RelayVRFDelay { .. } => {
m.cert.vrf = garbage_vrf_signature();
AssignmentCertKindV2::RelayVRFDelay { .. } => {
m.cert.vrf = vrf_signature;
Some(false)
},
_ => None, // skip everything else.
@@ -828,9 +1132,14 @@ mod tests {
#[test]
fn check_rejects_modulo_bad_vrf() {
check_mutated_assignments(200, 100, 25, |m| {
let vrf_signature = garbage_vrf_signature();
match m.cert.kind.clone() {
AssignmentCertKind::RelayVRFModulo { .. } => {
m.cert.vrf = garbage_vrf_signature();
AssignmentCertKindV2::RelayVRFModulo { .. } => {
m.cert.vrf = vrf_signature;
Some(false)
},
AssignmentCertKindV2::RelayVRFModuloCompact { .. } => {
m.cert.vrf = vrf_signature;
Some(false)
},
_ => None, // skip everything else.
@@ -842,10 +1151,11 @@ mod tests {
fn check_rejects_modulo_sample_out_of_bounds() {
check_mutated_assignments(200, 100, 25, |m| {
match m.cert.kind.clone() {
AssignmentCertKind::RelayVRFModulo { sample } => {
AssignmentCertKindV2::RelayVRFModulo { sample } => {
m.config.relay_vrf_modulo_samples = sample;
Some(false)
},
AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield: _ } => Some(true),
_ => None, // skip everything else.
}
});
@@ -855,8 +1165,11 @@ mod tests {
fn check_rejects_delay_claimed_core_wrong() {
check_mutated_assignments(200, 100, 25, |m| {
match m.cert.kind.clone() {
AssignmentCertKind::RelayVRFDelay { .. } => {
m.core = CoreIndex((m.core.0 + 1) % 100);
AssignmentCertKindV2::RelayVRFDelay { .. } => {
// for core in &mut m.cores {
// core.0 = (core.0 + 1) % 100;
// }
m.cores = CoreIndex((m.cores.first_one().unwrap() + 1) as u32 % 100).into();
Some(false)
},
_ => None, // skip everything else.
@@ -868,12 +1181,53 @@ mod tests {
fn check_rejects_modulo_core_wrong() {
check_mutated_assignments(200, 100, 25, |m| {
match m.cert.kind.clone() {
AssignmentCertKind::RelayVRFModulo { .. } => {
m.core = CoreIndex((m.core.0 + 1) % 100);
AssignmentCertKindV2::RelayVRFModulo { .. } |
AssignmentCertKindV2::RelayVRFModuloCompact { .. } => {
m.cores = CoreIndex((m.cores.first_one().unwrap() + 1) as u32 % 100).into();
Some(false)
},
_ => None, // skip everything else.
}
});
}
#[test]
fn generate_samples_invariant() {
let seed = [
1, 0, 52, 0, 0, 0, 0, 0, 1, 0, 10, 0, 22, 32, 0, 0, 2, 0, 55, 49, 0, 11, 0, 0, 3, 0, 0,
0, 0, 0, 2, 92,
];
let rand_chacha = ChaCha20Rng::from_seed(seed);
let samples = generate_samples(rand_chacha.clone(), 6, 100);
let expected = vec![19, 79, 17, 75, 66, 30].into_iter().map(Into::into).collect_vec();
assert_eq!(samples, expected);
let samples = generate_samples(rand_chacha.clone(), 6, 7);
let expected = vec![0, 3, 6, 5, 4, 2].into_iter().map(Into::into).collect_vec();
assert_eq!(samples, expected);
let samples = generate_samples(rand_chacha.clone(), 6, 12);
let expected = vec![2, 4, 7, 5, 11, 3].into_iter().map(Into::into).collect_vec();
assert_eq!(samples, expected);
let samples = generate_samples(rand_chacha.clone(), 1, 100);
let expected = vec![30].into_iter().map(Into::into).collect_vec();
assert_eq!(samples, expected);
let samples = generate_samples(rand_chacha.clone(), 0, 100);
let expected = vec![];
assert_eq!(samples, expected);
let samples = generate_samples(rand_chacha, MAX_MODULO_SAMPLES + 1, 100);
let expected = vec![
42, 54, 55, 93, 64, 27, 49, 15, 83, 71, 62, 1, 43, 77, 97, 41, 7, 69, 0, 88, 59, 14,
23, 87, 47, 4, 51, 12, 74, 56, 50, 44, 9, 82, 19, 79, 17, 75, 66, 30,
]
.into_iter()
.map(Into::into)
.collect_vec();
assert_eq!(samples, expected);
}
}
@@ -30,7 +30,10 @@
use polkadot_node_jaeger as jaeger;
use polkadot_node_primitives::{
approval::{self as approval_types, BlockApprovalMeta, RelayVRFStory},
approval::{
self as approval_types,
v1::{BlockApprovalMeta, RelayVRFStory},
},
MAX_FINALITY_LAG,
};
use polkadot_node_subsystem::{
@@ -53,7 +56,7 @@ use futures::{channel::oneshot, prelude::*};
use std::collections::HashMap;
use super::approval_db::v1;
use super::approval_db::v2;
use crate::{
backend::{Backend, OverlayedBackend},
criteria::{AssignmentCriteria, OurAssignment},
@@ -92,7 +95,7 @@ enum ImportedBlockInfoError {
FutureCancelled(&'static str, futures::channel::oneshot::Canceled),
#[error(transparent)]
ApprovalError(approval_types::ApprovalError),
ApprovalError(approval_types::v1::ApprovalError),
#[error("block is already finalized")]
BlockAlreadyFinalized,
@@ -216,7 +219,7 @@ async fn imported_block_info<Context>(
.ok_or(ImportedBlockInfoError::SessionInfoUnavailable)?;
let (assignments, slot, relay_vrf_story) = {
let unsafe_vrf = approval_types::babe_unsafe_vrf_info(&block_header);
let unsafe_vrf = approval_types::v1::babe_unsafe_vrf_info(&block_header);
match unsafe_vrf {
Some(unsafe_vrf) => {
@@ -497,7 +500,7 @@ pub(crate) async fn handle_new_head<Context, B: Backend>(
ctx.send_message(ChainSelectionMessage::Approved(block_hash)).await;
}
let block_entry = v1::BlockEntry {
let block_entry = v2::BlockEntry {
block_hash,
parent_hash: block_header.parent_hash,
block_number: block_header.number,
@@ -510,6 +513,7 @@ pub(crate) async fn handle_new_head<Context, B: Backend>(
.collect(),
approved_bitfield,
children: Vec::new(),
distributed_assignments: Default::default(),
};
gum::trace!(
@@ -588,11 +592,11 @@ pub(crate) async fn handle_new_head<Context, B: Backend>(
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::{approval_db::v1::DbBackend, RuntimeInfo, RuntimeInfoConfig};
use crate::{approval_db::v2::DbBackend, RuntimeInfo, RuntimeInfoConfig};
use ::test_helpers::{dummy_candidate_receipt, dummy_hash};
use assert_matches::assert_matches;
use polkadot_node_primitives::{
approval::{VrfSignature, VrfTranscript},
approval::v1::{VrfSignature, VrfTranscript},
DISPUTE_WINDOW,
};
use polkadot_node_subsystem::messages::{AllMessages, ApprovalVotingMessage};
@@ -610,7 +614,7 @@ pub(crate) mod tests {
pub(crate) use sp_runtime::{Digest, DigestItem};
use std::{pin::Pin, sync::Arc};
use crate::{approval_db::v1::Config as DatabaseConfig, criteria, BlockEntry};
use crate::{approval_db::v2::Config as DatabaseConfig, criteria, BlockEntry};
const DATA_COL: u32 = 0;
@@ -656,7 +660,7 @@ pub(crate) mod tests {
fn compute_assignments(
&self,
_keystore: &LocalKeystore,
_relay_vrf_story: polkadot_node_primitives::approval::RelayVRFStory,
_relay_vrf_story: polkadot_node_primitives::approval::v1::RelayVRFStory,
_config: &criteria::Config,
_leaving_cores: Vec<(
CandidateHash,
@@ -669,13 +673,14 @@ pub(crate) mod tests {
fn check_assignment_cert(
&self,
_claimed_core_index: polkadot_primitives::CoreIndex,
_claimed_core_bitfield: polkadot_node_primitives::approval::v2::CoreBitfield,
_validator_index: polkadot_primitives::ValidatorIndex,
_config: &criteria::Config,
_relay_vrf_story: polkadot_node_primitives::approval::RelayVRFStory,
_assignment: &polkadot_node_primitives::approval::AssignmentCert,
_backing_group: polkadot_primitives::GroupIndex,
) -> Result<polkadot_node_primitives::approval::DelayTranche, criteria::InvalidAssignment> {
_relay_vrf_story: polkadot_node_primitives::approval::v1::RelayVRFStory,
_assignment: &polkadot_node_primitives::approval::v2::AssignmentCertV2,
_backing_groups: Vec<polkadot_primitives::GroupIndex>,
) -> Result<polkadot_node_primitives::approval::v1::DelayTranche, criteria::InvalidAssignment>
{
Ok(0)
}
}
@@ -1296,7 +1301,7 @@ pub(crate) mod tests {
let (state, mut session_info_provider) = single_session_state();
overlay_db.write_block_entry(
v1::BlockEntry {
v2::BlockEntry {
block_hash: parent_hash,
parent_hash: Default::default(),
block_number: 4,
@@ -1306,6 +1311,7 @@ pub(crate) mod tests {
candidates: Vec::new(),
approved_bitfield: Default::default(),
children: Vec::new(),
distributed_assignments: Default::default(),
}
.into(),
);
@@ -1338,7 +1344,7 @@ pub(crate) mod tests {
// the first candidate should be insta-approved
// the second should not
let entry: BlockEntry =
v1::load_block_entry(db_writer.as_ref(), &TEST_CONFIG, &hash)
v2::load_block_entry(db_writer.as_ref(), &TEST_CONFIG, &hash)
.unwrap()
.unwrap()
.into();
+377 -143
View File
@@ -25,7 +25,11 @@ use jaeger::{hash_to_trace_identifier, PerLeafSpan};
use polkadot_node_jaeger as jaeger;
use polkadot_node_primitives::{
approval::{
BlockApprovalMeta, DelayTranche, IndirectAssignmentCert, IndirectSignedApprovalVote,
v1::{BlockApprovalMeta, DelayTranche, IndirectSignedApprovalVote},
v2::{
AssignmentCertKindV2, BitfieldError, CandidateBitfield, CoreBitfield,
IndirectAssignmentCertV2,
},
},
ValidationResult, DISPUTE_WINDOW,
};
@@ -76,12 +80,13 @@ use std::{
use schnellru::{ByLength, LruMap};
use approval_checking::RequiredTranches;
use bitvec::{order::Lsb0, vec::BitVec};
use criteria::{AssignmentCriteria, RealAssignmentCriteria};
use persisted_entries::{ApprovalEntry, BlockEntry, CandidateEntry};
use time::{slot_number_to_tick, Clock, ClockExt, SystemClock, Tick};
mod approval_checking;
mod approval_db;
pub mod approval_db;
mod backend;
mod criteria;
mod import;
@@ -90,8 +95,9 @@ mod persisted_entries;
mod time;
use crate::{
approval_db::v1::{Config as DatabaseConfig, DbBackend},
approval_db::v2::{Config as DatabaseConfig, DbBackend},
backend::{Backend, OverlayedBackend},
criteria::InvalidAssignmentReason,
};
#[cfg(test)]
@@ -107,7 +113,7 @@ const APPROVAL_CACHE_SIZE: u32 = 1024;
const TICK_TOO_FAR_IN_FUTURE: Tick = 20; // 10 seconds.
const APPROVAL_DELAY: Tick = 2;
const LOG_TARGET: &str = "parachain::approval-voting";
pub(crate) const LOG_TARGET: &str = "parachain::approval-voting";
/// Configuration for the approval voting subsystem
#[derive(Debug, Clone)]
@@ -377,8 +383,8 @@ impl ApprovalVotingSubsystem {
/// The operation is not allowed for blocks older than the last finalized one.
pub fn revert_to(&self, hash: Hash) -> Result<(), SubsystemError> {
let config =
approval_db::v1::Config { col_approval_data: self.db_config.col_approval_data };
let mut backend = approval_db::v1::DbBackend::new(self.db.clone(), config);
approval_db::v2::Config { col_approval_data: self.db_config.col_approval_data };
let mut backend = approval_db::v2::DbBackend::new(self.db.clone(), config);
let mut overlay = OverlayedBackend::new(&backend);
ops::revert_to(&mut overlay, hash)?;
@@ -754,15 +760,16 @@ enum Action {
tick: Tick,
},
LaunchApproval {
claimed_candidate_indices: CandidateBitfield,
candidate_hash: CandidateHash,
indirect_cert: IndirectAssignmentCert,
indirect_cert: IndirectAssignmentCertV2,
assignment_tranche: DelayTranche,
relay_block_hash: Hash,
candidate_index: CandidateIndex,
session: SessionIndex,
executor_params: ExecutorParams,
candidate: CandidateReceipt,
backing_group: GroupIndex,
distribute_assignment: bool,
},
NoteApprovedInChainSelection(Hash),
IssueApproval(CandidateHash, ApprovalVoteRequest),
@@ -977,15 +984,16 @@ async fn handle_actions<Context>(
actions_iter = next_actions.into_iter();
},
Action::LaunchApproval {
claimed_candidate_indices,
candidate_hash,
indirect_cert,
assignment_tranche,
relay_block_hash,
candidate_index,
session,
executor_params,
candidate,
backing_group,
distribute_assignment,
} => {
// Don't launch approval work if the node is syncing.
if let Mode::Syncing(_) = *mode {
@@ -1006,10 +1014,12 @@ async fn handle_actions<Context>(
launch_approval_span.add_string_tag("block-hash", format!("{:?}", block_hash));
let validator_index = indirect_cert.validator;
ctx.send_unbounded_message(ApprovalDistributionMessage::DistributeAssignment(
indirect_cert,
candidate_index,
));
if distribute_assignment {
ctx.send_unbounded_message(ApprovalDistributionMessage::DistributeAssignment(
indirect_cert,
claimed_candidate_indices,
));
}
match approvals_cache.get(&candidate_hash) {
Some(ApprovalOutcome::Approved) => {
@@ -1078,6 +1088,49 @@ async fn handle_actions<Context>(
Ok(conclude)
}
fn cores_to_candidate_indices(
core_indices: &CoreBitfield,
block_entry: &BlockEntry,
) -> Result<CandidateBitfield, BitfieldError> {
let mut candidate_indices = Vec::new();
// Map from core index to candidate index.
for claimed_core_index in core_indices.iter_ones() {
if let Some(candidate_index) = block_entry
.candidates()
.iter()
.position(|(core_index, _)| core_index.0 == claimed_core_index as u32)
{
candidate_indices.push(candidate_index as CandidateIndex);
}
}
CandidateBitfield::try_from(candidate_indices)
}
// Returns the claimed core bitfield from the assignment cert, the candidate hash and a
// `BlockEntry`. Can fail only for VRF Delay assignments for which we cannot find the candidate hash
// in the block entry which indicates a bug or corrupted storage.
fn get_assignment_core_indices(
assignment: &AssignmentCertKindV2,
candidate_hash: &CandidateHash,
block_entry: &BlockEntry,
) -> Option<CoreBitfield> {
match &assignment {
AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield } =>
Some(core_bitfield.clone()),
AssignmentCertKindV2::RelayVRFModulo { sample: _ } => block_entry
.candidates()
.iter()
.find(|(_core_index, h)| candidate_hash == h)
.map(|(core_index, _candidate_hash)| {
CoreBitfield::try_from(vec![*core_index]).expect("Not an empty vec; qed")
}),
AssignmentCertKindV2::RelayVRFDelay { core_index } =>
Some(CoreBitfield::try_from(vec![*core_index]).expect("Not an empty vec; qed")),
}
}
fn distribution_messages_for_activation(
db: &OverlayedBackend<'_, impl Backend>,
state: &State,
@@ -1142,33 +1195,95 @@ fn distribution_messages_for_activation(
match approval_entry.local_statements() {
(None, None) | (None, Some(_)) => {}, // second is impossible case.
(Some(assignment), None) => {
messages.push(ApprovalDistributionMessage::DistributeAssignment(
IndirectAssignmentCert {
block_hash,
validator: assignment.validator_index(),
cert: assignment.cert().clone(),
},
i as _,
));
if let Some(claimed_core_indices) = get_assignment_core_indices(
&assignment.cert().kind,
&candidate_hash,
&block_entry,
) {
match cores_to_candidate_indices(
&claimed_core_indices,
&block_entry,
) {
Ok(bitfield) => messages.push(
ApprovalDistributionMessage::DistributeAssignment(
IndirectAssignmentCertV2 {
block_hash,
validator: assignment.validator_index(),
cert: assignment.cert().clone(),
},
bitfield,
),
),
Err(err) => {
// Should never happen. If we fail here it means the
// assignment is null (no cores claimed).
gum::warn!(
target: LOG_TARGET,
?block_hash,
?candidate_hash,
?err,
"Failed to create assignment bitfield",
);
},
}
} else {
gum::warn!(
target: LOG_TARGET,
?block_hash,
?candidate_hash,
"Cannot get assignment claimed core indices",
);
}
},
(Some(assignment), Some(approval_sig)) => {
messages.push(ApprovalDistributionMessage::DistributeAssignment(
IndirectAssignmentCert {
block_hash,
validator: assignment.validator_index(),
cert: assignment.cert().clone(),
},
i as _,
));
if let Some(claimed_core_indices) = get_assignment_core_indices(
&assignment.cert().kind,
&candidate_hash,
&block_entry,
) {
match cores_to_candidate_indices(
&claimed_core_indices,
&block_entry,
) {
Ok(bitfield) => messages.push(
ApprovalDistributionMessage::DistributeAssignment(
IndirectAssignmentCertV2 {
block_hash,
validator: assignment.validator_index(),
cert: assignment.cert().clone(),
},
bitfield,
),
),
Err(err) => {
gum::warn!(
target: LOG_TARGET,
?block_hash,
?candidate_hash,
?err,
"Failed to create assignment bitfield",
);
// If we didn't send assignment, we don't send approval.
continue
},
}
messages.push(ApprovalDistributionMessage::DistributeApproval(
IndirectSignedApprovalVote {
block_hash,
candidate_index: i as _,
validator: assignment.validator_index(),
signature: approval_sig,
},
))
messages.push(ApprovalDistributionMessage::DistributeApproval(
IndirectSignedApprovalVote {
block_hash,
candidate_index: i as _,
validator: assignment.validator_index(),
signature: approval_sig,
},
));
} else {
gum::warn!(
target: LOG_TARGET,
?block_hash,
?candidate_hash,
"Cannot get assignment claimed core indices",
);
}
},
}
},
@@ -1288,14 +1403,14 @@ async fn handle_from_overseer<Context>(
vec![Action::Conclude]
},
FromOrchestra::Communication { msg } => match msg {
ApprovalVotingMessage::CheckAndImportAssignment(a, claimed_core, res) => {
ApprovalVotingMessage::CheckAndImportAssignment(a, claimed_cores, res) => {
let (check_outcome, actions) = check_and_import_assignment(
ctx.sender(),
state,
db,
session_info_provider,
a,
claimed_core,
claimed_cores,
)
.await?;
let _ = res.send(check_outcome);
@@ -1465,7 +1580,6 @@ async fn handle_approved_ancestor<Context>(
let mut span = span
.child("handle-approved-ancestor")
.with_stage(jaeger::Stage::ApprovalChecking);
use bitvec::{order::Lsb0, vec::BitVec};
let mut all_approved_max = None;
@@ -1804,8 +1918,8 @@ async fn check_and_import_assignment<Sender>(
state: &State,
db: &mut OverlayedBackend<'_, impl Backend>,
session_info_provider: &mut RuntimeInfo,
assignment: IndirectAssignmentCert,
candidate_index: CandidateIndex,
assignment: IndirectAssignmentCertV2,
candidate_indices: CandidateBitfield,
) -> SubsystemResult<(AssignmentCheckResult, Vec<Action>)>
where
Sender: SubsystemSender<RuntimeApiMessage>,
@@ -1818,9 +1932,12 @@ where
.map(|span| span.child("check-and-import-assignment"))
.unwrap_or_else(|| jaeger::Span::new(assignment.block_hash, "check-and-import-assignment"))
.with_relay_parent(assignment.block_hash)
.with_uint_tag("candidate-index", candidate_index as u64)
.with_stage(jaeger::Stage::ApprovalChecking);
for candidate_index in candidate_indices.iter_ones() {
check_and_import_assignment_span.add_uint_tag("candidate-index", candidate_index as u64);
}
let block_entry = match db.load_block_entry(&assignment.block_hash)? {
Some(b) => b,
None =>
@@ -1850,39 +1967,64 @@ where
)),
};
let (claimed_core_index, assigned_candidate_hash) =
match block_entry.candidate(candidate_index as usize) {
Some((c, h)) => (*c, *h),
let n_cores = session_info.n_cores as usize;
// Early check the candidate bitfield and core bitfields lengths < `n_cores`.
// Core bitfield length is checked later in `check_assignment_cert`.
if candidate_indices.len() > n_cores {
gum::debug!(
target: LOG_TARGET,
validator = assignment.validator.0,
n_cores,
candidate_bitfield_len = ?candidate_indices.len(),
"Oversized bitfield",
);
return Ok((
AssignmentCheckResult::Bad(AssignmentCheckError::InvalidBitfield(
candidate_indices.len(),
)),
Vec::new(),
))
}
// The Compact VRF modulo assignment cert has multiple core assignments.
let mut backing_groups = Vec::new();
let mut claimed_core_indices = Vec::new();
let mut assigned_candidate_hashes = Vec::new();
for candidate_index in candidate_indices.iter_ones() {
let (claimed_core_index, assigned_candidate_hash) =
match block_entry.candidate(candidate_index) {
Some((c, h)) => (*c, *h),
None =>
return Ok((
AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCandidateIndex(
candidate_index as _,
)),
Vec::new(),
)), // no candidate at core.
};
let mut candidate_entry = match db.load_candidate_entry(&assigned_candidate_hash)? {
Some(c) => c,
None =>
return Ok((
AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCandidateIndex(
candidate_index,
AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCandidate(
candidate_index as _,
assigned_candidate_hash,
)),
Vec::new(),
)), // no candidate at core.
};
check_and_import_assignment_span
.add_string_tag("candidate-hash", format!("{:?}", assigned_candidate_hash));
check_and_import_assignment_span.add_string_tag(
"traceID",
format!("{:?}", jaeger::hash_to_trace_identifier(assigned_candidate_hash.0)),
);
check_and_import_assignment_span
.add_string_tag("candidate-hash", format!("{:?}", assigned_candidate_hash));
check_and_import_assignment_span.add_string_tag(
"traceID",
format!("{:?}", jaeger::hash_to_trace_identifier(assigned_candidate_hash.0)),
);
let mut candidate_entry = match db.load_candidate_entry(&assigned_candidate_hash)? {
Some(c) => c,
None =>
return Ok((
AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCandidate(
candidate_index,
assigned_candidate_hash,
)),
Vec::new(),
)),
};
let res = {
// import the assignment.
let approval_entry = match candidate_entry.approval_entry_mut(&assignment.block_hash) {
Some(a) => a,
None =>
@@ -1895,79 +2037,144 @@ where
)),
};
let res = state.assignment_criteria.check_assignment_cert(
claimed_core_index,
assignment.validator,
&criteria::Config::from(session_info),
block_entry.relay_vrf_story(),
&assignment.cert,
approval_entry.backing_group(),
);
backing_groups.push(approval_entry.backing_group());
claimed_core_indices.push(claimed_core_index);
assigned_candidate_hashes.push(assigned_candidate_hash);
}
let tranche = match res {
Err(crate::criteria::InvalidAssignment(reason)) =>
return Ok((
AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCert(
assignment.validator,
format!("{:?}", reason),
)),
Vec::new(),
// Error on null assignments.
if claimed_core_indices.is_empty() {
return Ok((
AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCert(
assignment.validator,
format!("{:?}", InvalidAssignmentReason::NullAssignment),
)),
Vec::new(),
))
}
// Check the assignment certificate.
let res = state.assignment_criteria.check_assignment_cert(
claimed_core_indices
.clone()
.try_into()
.expect("Checked for null assignment above; qed"),
assignment.validator,
&criteria::Config::from(session_info),
block_entry.relay_vrf_story(),
&assignment.cert,
backing_groups,
);
let tranche = match res {
Err(crate::criteria::InvalidAssignment(reason)) =>
return Ok((
AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCert(
assignment.validator,
format!("{:?}", reason),
)),
Ok(tranche) => {
let current_tranche =
state.clock.tranche_now(state.slot_duration_millis, block_entry.slot());
Vec::new(),
)),
Ok(tranche) => {
let current_tranche =
state.clock.tranche_now(state.slot_duration_millis, block_entry.slot());
let too_far_in_future = current_tranche + TICK_TOO_FAR_IN_FUTURE as DelayTranche;
let too_far_in_future = current_tranche + TICK_TOO_FAR_IN_FUTURE as DelayTranche;
if tranche >= too_far_in_future {
return Ok((AssignmentCheckResult::TooFarInFuture, Vec::new()))
}
if tranche >= too_far_in_future {
return Ok((AssignmentCheckResult::TooFarInFuture, Vec::new()))
}
tranche
},
};
tranche
},
};
check_and_import_assignment_span.add_uint_tag("tranche", tranche as u64);
let mut actions = Vec::new();
let res = {
let mut is_duplicate = true;
// Import the assignments for all cores in the cert.
for (assigned_candidate_hash, candidate_index) in
assigned_candidate_hashes.iter().zip(candidate_indices.iter_ones())
{
let mut candidate_entry = match db.load_candidate_entry(&assigned_candidate_hash)? {
Some(c) => c,
None =>
return Ok((
AssignmentCheckResult::Bad(AssignmentCheckError::InvalidCandidate(
candidate_index as _,
*assigned_candidate_hash,
)),
Vec::new(),
)),
};
let is_duplicate = approval_entry.is_assigned(assignment.validator);
approval_entry.import_assignment(tranche, assignment.validator, tick_now);
let approval_entry = match candidate_entry.approval_entry_mut(&assignment.block_hash) {
Some(a) => a,
None =>
return Ok((
AssignmentCheckResult::Bad(AssignmentCheckError::Internal(
assignment.block_hash,
*assigned_candidate_hash,
)),
Vec::new(),
)),
};
is_duplicate &= approval_entry.is_assigned(assignment.validator);
approval_entry.import_assignment(tranche, assignment.validator, tick_now);
check_and_import_assignment_span.add_uint_tag("tranche", tranche as u64);
// We've imported a new assignment, so we need to schedule a wake-up for when that might
// no-show.
if let Some((approval_entry, status)) = state
.approval_status(sender, session_info_provider, &block_entry, &candidate_entry)
.await
{
actions.extend(schedule_wakeup_action(
approval_entry,
block_entry.block_hash(),
block_entry.block_number(),
*assigned_candidate_hash,
status.block_tick,
tick_now,
status.required_tranches,
));
}
// We also write the candidate entry as it now contains the new candidate.
db.write_candidate_entry(candidate_entry.into());
}
// Since we don't account for tranche in distribution message fingerprinting, some
// validators can be assigned to the same core (VRF modulo vs VRF delay). These can be
// safely ignored. However, if an assignment is for multiple cores (these are only
// tranche0), we cannot ignore it, because it would mean ignoring other non duplicate
// assignments.
if is_duplicate {
AssignmentCheckResult::AcceptedDuplicate
} else if candidate_indices.count_ones() > 1 {
gum::trace!(
target: LOG_TARGET,
validator = assignment.validator.0,
candidate_hashes = ?assigned_candidate_hashes,
assigned_cores = ?claimed_core_indices,
?tranche,
"Imported assignments for multiple cores.",
);
AssignmentCheckResult::Accepted
} else {
gum::trace!(
target: LOG_TARGET,
validator = assignment.validator.0,
candidate_hash = ?assigned_candidate_hash,
para_id = ?candidate_entry.candidate_receipt().descriptor.para_id,
"Imported assignment.",
candidate_hashes = ?assigned_candidate_hashes,
assigned_cores = ?claimed_core_indices,
"Imported assignment for a single core.",
);
AssignmentCheckResult::Accepted
}
};
let mut actions = Vec::new();
// We've imported a new approval, so we need to schedule a wake-up for when that might no-show.
if let Some((approval_entry, status)) = state
.approval_status(sender, session_info_provider, &block_entry, &candidate_entry)
.await
{
actions.extend(schedule_wakeup_action(
approval_entry,
block_entry.block_hash(),
block_entry.block_number(),
assigned_candidate_hash,
status.block_tick,
tick_now,
status.required_tranches,
));
}
// We also write the candidate entry as it now contains the new candidate.
db.write_candidate_entry(candidate_entry.into());
Ok((res, actions))
}
@@ -2341,7 +2548,7 @@ async fn process_wakeup<Context>(
let candidate_entry = db.load_candidate_entry(&candidate_hash)?;
// If either is not present, we have nothing to wakeup. Might have lost a race with finality
let (block_entry, mut candidate_entry) = match (block_entry, candidate_entry) {
let (mut block_entry, mut candidate_entry) = match (block_entry, candidate_entry) {
(Some(b), Some(c)) => (b, c),
_ => return Ok(Vec::new()),
};
@@ -2422,32 +2629,59 @@ async fn process_wakeup<Context>(
if let Some((cert, val_index, tranche)) = maybe_cert {
let indirect_cert =
IndirectAssignmentCert { block_hash: relay_block, validator: val_index, cert };
IndirectAssignmentCertV2 { block_hash: relay_block, validator: val_index, cert };
let index_in_candidate =
block_entry.candidates().iter().position(|(_, h)| &candidate_hash == h);
gum::trace!(
target: LOG_TARGET,
?candidate_hash,
para_id = ?candidate_receipt.descriptor.para_id,
block_hash = ?relay_block,
"Launching approval work.",
);
if let Some(i) = index_in_candidate {
gum::trace!(
if let Some(claimed_core_indices) =
get_assignment_core_indices(&indirect_cert.cert.kind, &candidate_hash, &block_entry)
{
match cores_to_candidate_indices(&claimed_core_indices, &block_entry) {
Ok(claimed_candidate_indices) => {
// Ensure we distribute multiple core assignments just once.
let distribute_assignment = if claimed_candidate_indices.count_ones() > 1 {
!block_entry.mark_assignment_distributed(claimed_candidate_indices.clone())
} else {
true
};
db.write_block_entry(block_entry.clone());
actions.push(Action::LaunchApproval {
claimed_candidate_indices,
candidate_hash,
indirect_cert,
assignment_tranche: tranche,
relay_block_hash: relay_block,
session: block_entry.session(),
executor_params: executor_params.clone(),
candidate: candidate_receipt,
backing_group,
distribute_assignment,
});
},
Err(err) => {
// Never happens, it should only happen if no cores are claimed, which is a bug.
gum::warn!(
target: LOG_TARGET,
block_hash = ?relay_block,
?err,
"Failed to create assignment bitfield"
);
},
};
} else {
gum::warn!(
target: LOG_TARGET,
?candidate_hash,
para_id = ?candidate_receipt.descriptor.para_id,
block_hash = ?relay_block,
"Launching approval work.",
?candidate_hash,
"Cannot get assignment claimed core indices",
);
// sanity: should always be present.
actions.push(Action::LaunchApproval {
candidate_hash,
indirect_cert,
assignment_tranche: tranche,
relay_block_hash: relay_block,
candidate_index: i as _,
session: block_entry.session(),
executor_params: executor_params.clone(),
candidate: candidate_receipt,
backing_group,
});
}
}
// Although we checked approval earlier in this function,
@@ -25,7 +25,7 @@ use polkadot_primitives::{BlockNumber, CandidateHash, CandidateReceipt, GroupInd
use std::collections::{hash_map::Entry, BTreeMap, HashMap};
use super::{
approval_db::v1::{OurAssignment, StoredBlockRange},
approval_db::v2::{OurAssignment, StoredBlockRange},
backend::{Backend, OverlayedBackend},
persisted_entries::{ApprovalEntry, BlockEntry, CandidateEntry},
LOG_TARGET,
@@ -20,16 +20,21 @@
//! Within that context, things are plain-old-data. Within this module,
//! data and logic are intertwined.
use polkadot_node_primitives::approval::{AssignmentCert, DelayTranche, RelayVRFStory};
use polkadot_node_primitives::approval::{
v1::{DelayTranche, RelayVRFStory},
v2::{AssignmentCertV2, CandidateBitfield},
};
use polkadot_primitives::{
BlockNumber, CandidateHash, CandidateReceipt, CoreIndex, GroupIndex, Hash, SessionIndex,
ValidatorIndex, ValidatorSignature,
};
use sp_consensus_slots::Slot;
use bitvec::{order::Lsb0 as BitOrderLsb0, slice::BitSlice, vec::BitVec};
use bitvec::{order::Lsb0 as BitOrderLsb0, slice::BitSlice};
use std::collections::BTreeMap;
use crate::approval_db::v2::Bitfield;
use super::{criteria::OurAssignment, time::Tick};
/// Metadata regarding a specific tranche of assignments for a specific candidate.
@@ -53,8 +58,8 @@ impl TrancheEntry {
}
}
impl From<crate::approval_db::v1::TrancheEntry> for TrancheEntry {
fn from(entry: crate::approval_db::v1::TrancheEntry) -> Self {
impl From<crate::approval_db::v2::TrancheEntry> for TrancheEntry {
fn from(entry: crate::approval_db::v2::TrancheEntry) -> Self {
TrancheEntry {
tranche: entry.tranche,
assignments: entry.assignments.into_iter().map(|(v, t)| (v, t.into())).collect(),
@@ -62,7 +67,7 @@ impl From<crate::approval_db::v1::TrancheEntry> for TrancheEntry {
}
}
impl From<TrancheEntry> for crate::approval_db::v1::TrancheEntry {
impl From<TrancheEntry> for crate::approval_db::v2::TrancheEntry {
fn from(entry: TrancheEntry) -> Self {
Self {
tranche: entry.tranche,
@@ -80,7 +85,7 @@ pub struct ApprovalEntry {
our_assignment: Option<OurAssignment>,
our_approval_sig: Option<ValidatorSignature>,
// `n_validators` bits.
assignments: BitVec<u8, BitOrderLsb0>,
assigned_validators: Bitfield,
approved: bool,
}
@@ -92,10 +97,17 @@ impl ApprovalEntry {
our_assignment: Option<OurAssignment>,
our_approval_sig: Option<ValidatorSignature>,
// `n_validators` bits.
assignments: BitVec<u8, BitOrderLsb0>,
assigned_validators: Bitfield,
approved: bool,
) -> Self {
Self { tranches, backing_group, our_assignment, our_approval_sig, assignments, approved }
Self {
tranches,
backing_group,
our_assignment,
our_approval_sig,
assigned_validators,
approved,
}
}
// Access our assignment for this approval entry.
@@ -107,7 +119,7 @@ impl ApprovalEntry {
pub fn trigger_our_assignment(
&mut self,
tick_now: Tick,
) -> Option<(AssignmentCert, ValidatorIndex, DelayTranche)> {
) -> Option<(AssignmentCertV2, ValidatorIndex, DelayTranche)> {
let our = self.our_assignment.as_mut().and_then(|a| {
if a.triggered() {
return None
@@ -131,7 +143,10 @@ impl ApprovalEntry {
/// Whether a validator is already assigned.
pub fn is_assigned(&self, validator_index: ValidatorIndex) -> bool {
self.assignments.get(validator_index.0 as usize).map(|b| *b).unwrap_or(false)
self.assigned_validators
.get(validator_index.0 as usize)
.map(|b| *b)
.unwrap_or(false)
}
/// Import an assignment. No-op if already assigned on the same tranche.
@@ -158,14 +173,14 @@ impl ApprovalEntry {
};
self.tranches[idx].assignments.push((validator_index, tick_now));
self.assignments.set(validator_index.0 as _, true);
self.assigned_validators.set(validator_index.0 as _, true);
}
// Produce a bitvec indicating the assignments of all validators up to and
// including `tranche`.
pub fn assignments_up_to(&self, tranche: DelayTranche) -> BitVec<u8, BitOrderLsb0> {
pub fn assignments_up_to(&self, tranche: DelayTranche) -> Bitfield {
self.tranches.iter().take_while(|e| e.tranche <= tranche).fold(
bitvec::bitvec![u8, BitOrderLsb0; 0; self.assignments.len()],
bitvec::bitvec![u8, BitOrderLsb0; 0; self.assigned_validators.len()],
|mut a, e| {
for &(v, _) in &e.assignments {
a.set(v.0 as _, true);
@@ -193,12 +208,12 @@ impl ApprovalEntry {
/// Get the number of validators in this approval entry.
pub fn n_validators(&self) -> usize {
self.assignments.len()
self.assigned_validators.len()
}
/// Get the number of assignments by validators, including the local validator.
pub fn n_assignments(&self) -> usize {
self.assignments.count_ones()
self.assigned_validators.count_ones()
}
/// Get the backing group index of the approval entry.
@@ -219,27 +234,27 @@ impl ApprovalEntry {
}
}
impl From<crate::approval_db::v1::ApprovalEntry> for ApprovalEntry {
fn from(entry: crate::approval_db::v1::ApprovalEntry) -> Self {
impl From<crate::approval_db::v2::ApprovalEntry> for ApprovalEntry {
fn from(entry: crate::approval_db::v2::ApprovalEntry) -> Self {
ApprovalEntry {
tranches: entry.tranches.into_iter().map(Into::into).collect(),
backing_group: entry.backing_group,
our_assignment: entry.our_assignment.map(Into::into),
our_approval_sig: entry.our_approval_sig.map(Into::into),
assignments: entry.assignments,
assigned_validators: entry.assigned_validators,
approved: entry.approved,
}
}
}
impl From<ApprovalEntry> for crate::approval_db::v1::ApprovalEntry {
impl From<ApprovalEntry> for crate::approval_db::v2::ApprovalEntry {
fn from(entry: ApprovalEntry) -> Self {
Self {
tranches: entry.tranches.into_iter().map(Into::into).collect(),
backing_group: entry.backing_group,
our_assignment: entry.our_assignment.map(Into::into),
our_approval_sig: entry.our_approval_sig.map(Into::into),
assignments: entry.assignments,
assigned_validators: entry.assigned_validators,
approved: entry.approved,
}
}
@@ -253,7 +268,7 @@ pub struct CandidateEntry {
// Assignments are based on blocks, so we need to track assignments separately
// based on the block we are looking at.
pub block_assignments: BTreeMap<Hash, ApprovalEntry>,
pub approvals: BitVec<u8, BitOrderLsb0>,
pub approvals: Bitfield,
}
impl CandidateEntry {
@@ -290,8 +305,8 @@ impl CandidateEntry {
}
}
impl From<crate::approval_db::v1::CandidateEntry> for CandidateEntry {
fn from(entry: crate::approval_db::v1::CandidateEntry) -> Self {
impl From<crate::approval_db::v2::CandidateEntry> for CandidateEntry {
fn from(entry: crate::approval_db::v2::CandidateEntry) -> Self {
CandidateEntry {
candidate: entry.candidate,
session: entry.session,
@@ -305,7 +320,7 @@ impl From<crate::approval_db::v1::CandidateEntry> for CandidateEntry {
}
}
impl From<CandidateEntry> for crate::approval_db::v1::CandidateEntry {
impl From<CandidateEntry> for crate::approval_db::v2::CandidateEntry {
fn from(entry: CandidateEntry) -> Self {
Self {
candidate: entry.candidate,
@@ -336,8 +351,12 @@ pub struct BlockEntry {
// A bitfield where the i'th bit corresponds to the i'th candidate in `candidates`.
// The i'th bit is `true` iff the candidate has been approved in the context of this
// block. The block can be considered approved if the bitfield has all bits set to `true`.
pub approved_bitfield: BitVec<u8, BitOrderLsb0>,
pub approved_bitfield: Bitfield,
pub children: Vec<Hash>,
// A list of assignments for which we already distributed the assignment.
// We use this to ensure we don't distribute multiple core assignments twice as we track
// individual wakeups for each core.
distributed_assignments: Bitfield,
}
impl BlockEntry {
@@ -412,6 +431,39 @@ impl BlockEntry {
pub fn parent_hash(&self) -> Hash {
self.parent_hash
}
/// Mark distributed assignment for many candidate indices.
/// Returns `true` if an assignment was already distributed for the `candidates`.
pub fn mark_assignment_distributed(&mut self, candidates: CandidateBitfield) -> bool {
let bitfield = candidates.into_inner();
let total_one_bits = self.distributed_assignments.count_ones();
let new_len = std::cmp::max(self.distributed_assignments.len(), bitfield.len());
self.distributed_assignments.resize(new_len, false);
self.distributed_assignments |= bitfield;
// If the an operation did not change our current bitfied, we return true.
let distributed = total_one_bits == self.distributed_assignments.count_ones();
distributed
}
}
impl From<crate::approval_db::v2::BlockEntry> for BlockEntry {
fn from(entry: crate::approval_db::v2::BlockEntry) -> Self {
BlockEntry {
block_hash: entry.block_hash,
parent_hash: entry.parent_hash,
block_number: entry.block_number,
session: entry.session,
slot: entry.slot,
relay_vrf_story: RelayVRFStory(entry.relay_vrf_story),
candidates: entry.candidates,
approved_bitfield: entry.approved_bitfield,
children: entry.children,
distributed_assignments: entry.distributed_assignments,
}
}
}
impl From<crate::approval_db::v1::BlockEntry> for BlockEntry {
@@ -426,11 +478,12 @@ impl From<crate::approval_db::v1::BlockEntry> for BlockEntry {
candidates: entry.candidates,
approved_bitfield: entry.approved_bitfield,
children: entry.children,
distributed_assignments: Default::default(),
}
}
}
impl From<BlockEntry> for crate::approval_db::v1::BlockEntry {
impl From<BlockEntry> for crate::approval_db::v2::BlockEntry {
fn from(entry: BlockEntry) -> Self {
Self {
block_hash: entry.block_hash,
@@ -442,6 +495,36 @@ impl From<BlockEntry> for crate::approval_db::v1::BlockEntry {
candidates: entry.candidates,
approved_bitfield: entry.approved_bitfield,
children: entry.children,
distributed_assignments: entry.distributed_assignments,
}
}
}
/// Migration helpers.
impl From<crate::approval_db::v1::CandidateEntry> for CandidateEntry {
fn from(value: crate::approval_db::v1::CandidateEntry) -> Self {
Self {
approvals: value.approvals,
block_assignments: value
.block_assignments
.into_iter()
.map(|(h, ae)| (h, ae.into()))
.collect(),
candidate: value.candidate,
session: value.session,
}
}
}
impl From<crate::approval_db::v1::ApprovalEntry> for ApprovalEntry {
fn from(value: crate::approval_db::v1::ApprovalEntry) -> Self {
ApprovalEntry {
tranches: value.tranches.into_iter().map(|tranche| tranche.into()).collect(),
backing_group: value.backing_group,
our_assignment: value.our_assignment.map(|assignment| assignment.into()),
our_approval_sig: value.our_approval_sig,
assigned_validators: value.assignments,
approved: value.approved,
}
}
}
+357 -40
View File
@@ -16,10 +16,14 @@
use self::test_helpers::mock::new_leaf;
use super::*;
use crate::backend::V1ReadBackend;
use polkadot_node_primitives::{
approval::{
AssignmentCert, AssignmentCertKind, DelayTranche, VrfOutput, VrfProof, VrfSignature,
RELAY_VRF_MODULO_CONTEXT,
v1::{
AssignmentCert, AssignmentCertKind, DelayTranche, VrfOutput, VrfProof, VrfSignature,
RELAY_VRF_MODULO_CONTEXT,
},
v2::{AssignmentCertKindV2, AssignmentCertV2},
},
AvailableData, BlockData, PoV,
};
@@ -52,7 +56,7 @@ use std::{
};
use super::{
approval_db::v1::StoredBlockRange,
approval_db::v2::StoredBlockRange,
backend::BackendWriteOp,
import::tests::{
garbage_vrf_signature, AllowedSlots, BabeEpoch, BabeEpochConfiguration,
@@ -112,7 +116,7 @@ fn make_sync_oracle(val: bool) -> (Box<dyn SyncOracle + Send>, TestSyncOracleHan
#[cfg(test)]
pub mod test_constants {
use crate::approval_db::v1::Config as DatabaseConfig;
use crate::approval_db::v2::Config as DatabaseConfig;
const DATA_COL: u32 = 0;
pub(crate) const NUM_COLUMNS: u32 = 1;
@@ -162,7 +166,7 @@ impl Clock for MockClock {
// This mock clock allows us to manipulate the time and
// be notified when wakeups have been triggered.
#[derive(Default)]
#[derive(Default, Debug)]
struct MockClockInner {
tick: Tick,
wakeups: Vec<(Tick, oneshot::Sender<()>)>,
@@ -232,7 +236,7 @@ where
fn compute_assignments(
&self,
_keystore: &LocalKeystore,
_relay_vrf_story: polkadot_node_primitives::approval::RelayVRFStory,
_relay_vrf_story: polkadot_node_primitives::approval::v1::RelayVRFStory,
_config: &criteria::Config,
_leaving_cores: Vec<(
CandidateHash,
@@ -245,13 +249,13 @@ where
fn check_assignment_cert(
&self,
_claimed_core_index: polkadot_primitives::CoreIndex,
_claimed_core_bitfield: polkadot_node_primitives::approval::v2::CoreBitfield,
validator_index: ValidatorIndex,
_config: &criteria::Config,
_relay_vrf_story: polkadot_node_primitives::approval::RelayVRFStory,
_assignment: &polkadot_node_primitives::approval::AssignmentCert,
_backing_group: polkadot_primitives::GroupIndex,
) -> Result<polkadot_node_primitives::approval::DelayTranche, criteria::InvalidAssignment> {
_relay_vrf_story: polkadot_node_primitives::approval::v1::RelayVRFStory,
_assignment: &polkadot_node_primitives::approval::v2::AssignmentCertV2,
_backing_groups: Vec<polkadot_primitives::GroupIndex>,
) -> Result<polkadot_node_primitives::approval::v1::DelayTranche, criteria::InvalidAssignment> {
self.1(validator_index)
}
}
@@ -272,6 +276,18 @@ struct TestStoreInner {
candidate_entries: HashMap<CandidateHash, CandidateEntry>,
}
impl V1ReadBackend for TestStoreInner {
fn load_candidate_entry_v1(
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>> {
self.load_candidate_entry(candidate_hash)
}
fn load_block_entry_v1(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>> {
self.load_block_entry(block_hash)
}
}
impl Backend for TestStoreInner {
fn load_block_entry(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>> {
Ok(self.block_entries.get(block_hash).cloned())
@@ -343,6 +359,18 @@ pub struct TestStore {
store: Arc<Mutex<TestStoreInner>>,
}
impl V1ReadBackend for TestStore {
fn load_candidate_entry_v1(
&self,
candidate_hash: &CandidateHash,
) -> SubsystemResult<Option<CandidateEntry>> {
self.load_candidate_entry(candidate_hash)
}
fn load_block_entry_v1(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>> {
self.load_block_entry(block_hash)
}
}
impl Backend for TestStore {
fn load_block_entry(&self, block_hash: &Hash) -> SubsystemResult<Option<BlockEntry>> {
let store = self.store.lock();
@@ -392,6 +420,17 @@ fn garbage_assignment_cert(kind: AssignmentCertKind) -> AssignmentCert {
AssignmentCert { kind, vrf: VrfSignature { output: VrfOutput(out), proof: VrfProof(proof) } }
}
fn garbage_assignment_cert_v2(kind: AssignmentCertKindV2) -> AssignmentCertV2 {
let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT);
let msg = b"test-garbage";
let mut prng = rand_core::OsRng;
let keypair = schnorrkel::Keypair::generate_with(&mut prng);
let (inout, proof, _) = keypair.vrf_sign(ctx.bytes(msg));
let out = inout.to_output();
AssignmentCertV2 { kind, vrf: VrfSignature { output: VrfOutput(out), proof: VrfProof(proof) } }
}
fn sign_approval(
key: Sr25519Keyring,
candidate_hash: CandidateHash,
@@ -468,6 +507,12 @@ fn test_harness<T: Future<Output = VirtualOverseer>>(
config: HarnessConfig,
test: impl FnOnce(TestHarness) -> T,
) {
let _ = env_logger::builder()
.is_test(true)
.filter(Some("polkadot_node_core_approval_voting"), log::LevelFilter::Trace)
.filter(Some(LOG_TARGET), log::LevelFilter::Trace)
.try_init();
let HarnessConfig { sync_oracle, sync_oracle_handle, clock, backend, assignment_criteria } =
config;
@@ -617,12 +662,13 @@ async fn check_and_import_assignment(
overseer,
FromOrchestra::Communication {
msg: ApprovalVotingMessage::CheckAndImportAssignment(
IndirectAssignmentCert {
IndirectAssignmentCertV2 {
block_hash,
validator,
cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 })
.into(),
},
candidate_index,
candidate_index.into(),
tx,
),
},
@@ -631,6 +677,38 @@ async fn check_and_import_assignment(
rx
}
async fn check_and_import_assignment_v2(
overseer: &mut VirtualOverseer,
block_hash: Hash,
core_indices: Vec<u32>,
validator: ValidatorIndex,
) -> oneshot::Receiver<AssignmentCheckResult> {
let (tx, rx) = oneshot::channel();
overseer_send(
overseer,
FromOrchestra::Communication {
msg: ApprovalVotingMessage::CheckAndImportAssignment(
IndirectAssignmentCertV2 {
block_hash,
validator,
cert: garbage_assignment_cert_v2(AssignmentCertKindV2::RelayVRFModuloCompact {
core_bitfield: core_indices
.clone()
.into_iter()
.map(|c| CoreIndex(c))
.collect::<Vec<_>>()
.try_into()
.unwrap(),
}),
},
core_indices.try_into().unwrap(),
tx,
),
},
)
.await;
rx
}
struct BlockConfig {
slot: Slot,
candidates: Option<Vec<(CandidateReceipt, CoreIndex, GroupIndex)>>,
@@ -743,7 +821,7 @@ fn session_info(keys: &[Sr25519Keyring]) -> SessionInfo {
vec![ValidatorIndex(0)],
vec![ValidatorIndex(1)],
]),
n_cores: keys.len() as _,
n_cores: 10,
needed_approvals: 2,
zeroth_delay_tranche_width: 5,
relay_vrf_modulo_samples: 3,
@@ -1068,14 +1146,15 @@ fn blank_subsystem_act_on_bad_block() {
&mut virtual_overseer,
FromOrchestra::Communication {
msg: ApprovalVotingMessage::CheckAndImportAssignment(
IndirectAssignmentCert {
IndirectAssignmentCertV2 {
block_hash: bad_block_hash,
validator: 0u32.into(),
cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
sample: 0,
}),
})
.into(),
},
0u32,
0u32.into(),
tx,
),
},
@@ -1331,9 +1410,22 @@ fn subsystem_accepts_duplicate_assignment() {
}
);
let block_hash = Hash::repeat_byte(0x01);
let candidate_index = 0;
let validator = ValidatorIndex(0);
let candidate_index = 0;
let block_hash = Hash::repeat_byte(0x01);
let candidate_receipt1 = {
let mut receipt = dummy_candidate_receipt(block_hash);
receipt.descriptor.para_id = ParaId::from(1_u32);
receipt
};
let candidate_receipt2 = {
let mut receipt = dummy_candidate_receipt(block_hash);
receipt.descriptor.para_id = ParaId::from(2_u32);
receipt
};
let candidate_index1 = 0;
let candidate_index2 = 1;
// Add block hash 00.
ChainBuilder::new()
@@ -1341,11 +1433,30 @@ fn subsystem_accepts_duplicate_assignment() {
block_hash,
ChainBuilder::GENESIS_HASH,
1,
BlockConfig { slot: Slot::from(1), candidates: None, session_info: None },
BlockConfig {
slot: Slot::from(1),
candidates: Some(vec![
(candidate_receipt1, CoreIndex(0), GroupIndex(1)),
(candidate_receipt2, CoreIndex(1), GroupIndex(1)),
]),
session_info: None,
},
)
.build(&mut virtual_overseer)
.await;
// Initial assignment.
let rx = check_and_import_assignment_v2(
&mut virtual_overseer,
block_hash,
vec![candidate_index1, candidate_index2],
validator,
)
.await;
assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
// Test with single assigned core.
let rx = check_and_import_assignment(
&mut virtual_overseer,
block_hash,
@@ -1354,12 +1465,14 @@ fn subsystem_accepts_duplicate_assignment() {
)
.await;
assert_eq!(rx.await, Ok(AssignmentCheckResult::Accepted));
assert_eq!(rx.await, Ok(AssignmentCheckResult::AcceptedDuplicate));
let rx = check_and_import_assignment(
// Test with multiple assigned cores. This cannot happen in practice, as tranche0
// assignments are sent first, but we should still ensure correct behavior.
let rx = check_and_import_assignment_v2(
&mut virtual_overseer,
block_hash,
candidate_index,
vec![candidate_index1, candidate_index2],
validator,
)
.await;
@@ -1416,6 +1529,63 @@ fn subsystem_rejects_assignment_with_unknown_candidate() {
});
}
#[test]
fn subsystem_rejects_oversized_bitfields() {
test_harness(HarnessConfig::default(), |test_harness| async move {
let TestHarness { mut virtual_overseer, sync_oracle_handle: _sync_oracle_handle, .. } =
test_harness;
assert_matches!(
overseer_recv(&mut virtual_overseer).await,
AllMessages::ChainApi(ChainApiMessage::FinalizedBlockNumber(rx)) => {
rx.send(Ok(0)).unwrap();
}
);
let block_hash = Hash::repeat_byte(0x01);
let candidate_index = 10;
let validator = ValidatorIndex(0);
ChainBuilder::new()
.add_block(
block_hash,
ChainBuilder::GENESIS_HASH,
1,
BlockConfig { slot: Slot::from(1), candidates: None, session_info: None },
)
.build(&mut virtual_overseer)
.await;
let rx = check_and_import_assignment(
&mut virtual_overseer,
block_hash,
candidate_index,
validator,
)
.await;
assert_eq!(
rx.await,
Ok(AssignmentCheckResult::Bad(AssignmentCheckError::InvalidBitfield(
candidate_index as usize + 1
))),
);
let rx = check_and_import_assignment_v2(
&mut virtual_overseer,
block_hash,
vec![1, 2, 10, 50],
validator,
)
.await;
assert_eq!(
rx.await,
Ok(AssignmentCheckResult::Bad(AssignmentCheckError::InvalidBitfield(51))),
);
virtual_overseer
});
}
#[test]
fn subsystem_accepts_and_imports_approval_after_assignment() {
test_harness(HarnessConfig::default(), |test_harness| async move {
@@ -1736,14 +1906,15 @@ fn linear_import_act_on_leaf() {
&mut virtual_overseer,
FromOrchestra::Communication {
msg: ApprovalVotingMessage::CheckAndImportAssignment(
IndirectAssignmentCert {
IndirectAssignmentCertV2 {
block_hash: head,
validator: 0u32.into(),
cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
sample: 0,
}),
})
.into(),
},
0u32,
0u32.into(),
tx,
),
},
@@ -1806,14 +1977,15 @@ fn forkful_import_at_same_height_act_on_leaf() {
&mut virtual_overseer,
FromOrchestra::Communication {
msg: ApprovalVotingMessage::CheckAndImportAssignment(
IndirectAssignmentCert {
IndirectAssignmentCertV2 {
block_hash: head,
validator: 0u32.into(),
cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo {
sample: 0,
}),
})
.into(),
},
0u32,
0u32.into(),
tx,
),
},
@@ -2257,8 +2429,24 @@ fn subsystem_validate_approvals_cache() {
let mut assignments = HashMap::new();
let _ = assignments.insert(
CoreIndex(0),
approval_db::v1::OurAssignment {
cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
approval_db::v2::OurAssignment {
cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 })
.into(),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
}
.into(),
);
let _ = assignments.insert(
CoreIndex(0),
approval_db::v2::OurAssignment {
cert: garbage_assignment_cert_v2(AssignmentCertKindV2::RelayVRFModuloCompact {
core_bitfield: vec![CoreIndex(0), CoreIndex(1), CoreIndex(2)]
.try_into()
.unwrap(),
}),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
@@ -2355,6 +2543,137 @@ fn subsystem_validate_approvals_cache() {
});
}
#[test]
fn subsystem_doesnt_distribute_duplicate_compact_assignments() {
let assignment_criteria = Box::new(MockAssignmentCriteria(
|| {
let mut assignments = HashMap::new();
let cert = garbage_assignment_cert_v2(AssignmentCertKindV2::RelayVRFModuloCompact {
core_bitfield: vec![CoreIndex(0), CoreIndex(1)].try_into().unwrap(),
});
let _ = assignments.insert(
CoreIndex(0),
approval_db::v2::OurAssignment {
cert: cert.clone(),
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
}
.into(),
);
let _ = assignments.insert(
CoreIndex(1),
approval_db::v2::OurAssignment {
cert,
tranche: 0,
validator_index: ValidatorIndex(0),
triggered: false,
}
.into(),
);
assignments
},
|_| Ok(0),
));
let config = HarnessConfigBuilder::default().assignment_criteria(assignment_criteria).build();
let store = config.backend();
test_harness(config, |test_harness| async move {
let TestHarness {
mut virtual_overseer,
sync_oracle_handle: _sync_oracle_handle,
clock,
..
} = test_harness;
assert_matches!(
overseer_recv(&mut virtual_overseer).await,
AllMessages::ChainApi(ChainApiMessage::FinalizedBlockNumber(rx)) => {
rx.send(Ok(0)).unwrap();
}
);
let block_hash = Hash::repeat_byte(0x01);
let candidate_receipt1 = {
let mut receipt = dummy_candidate_receipt(block_hash);
receipt.descriptor.para_id = ParaId::from(1_u32);
receipt
};
let candidate_receipt2 = {
let mut receipt = dummy_candidate_receipt(block_hash);
receipt.descriptor.para_id = ParaId::from(2_u32);
receipt
};
let candidate_index1 = 0;
let candidate_index2 = 1;
// Add block hash 00.
ChainBuilder::new()
.add_block(
block_hash,
ChainBuilder::GENESIS_HASH,
1,
BlockConfig {
slot: Slot::from(0),
candidates: Some(vec![
(candidate_receipt1.clone(), CoreIndex(0), GroupIndex(1)),
(candidate_receipt2.clone(), CoreIndex(1), GroupIndex(1)),
]),
session_info: None,
},
)
.build(&mut virtual_overseer)
.await;
// Activate the wakeup present above, and sleep to allow process_wakeups to execute..
assert_eq!(Some(2), clock.inner.lock().next_wakeup());
gum::trace!("clock \n{:?}\n", clock.inner.lock());
clock.inner.lock().wakeup_all(100);
assert_eq!(clock.inner.lock().wakeups.len(), 0);
futures_timer::Delay::new(Duration::from_millis(100)).await;
// Assignment is distributed only once from `approval-voting`
assert_matches!(
overseer_recv(&mut virtual_overseer).await,
AllMessages::ApprovalDistribution(ApprovalDistributionMessage::DistributeAssignment(
_,
c_indices,
)) => {
assert_eq!(c_indices, vec![candidate_index1, candidate_index2].try_into().unwrap());
}
);
// Candidate 1
recover_available_data(&mut virtual_overseer).await;
fetch_validation_code(&mut virtual_overseer).await;
// Candidate 2
recover_available_data(&mut virtual_overseer).await;
fetch_validation_code(&mut virtual_overseer).await;
// Check if assignment was triggered for candidate 1.
let candidate_entry =
store.load_candidate_entry(&candidate_receipt1.hash()).unwrap().unwrap();
let our_assignment =
candidate_entry.approval_entry(&block_hash).unwrap().our_assignment().unwrap();
assert!(our_assignment.triggered());
// Check if assignment was triggered for candidate 2.
let candidate_entry =
store.load_candidate_entry(&candidate_receipt2.hash()).unwrap().unwrap();
let our_assignment =
candidate_entry.approval_entry(&block_hash).unwrap().our_assignment().unwrap();
assert!(our_assignment.triggered());
virtual_overseer
});
}
/// Ensure that when two assignments are imported, only one triggers the Approval Checking work
async fn handle_double_assignment_import(
virtual_overseer: &mut VirtualOverseer,
@@ -2364,9 +2683,9 @@ async fn handle_double_assignment_import(
overseer_recv(virtual_overseer).await,
AllMessages::ApprovalDistribution(ApprovalDistributionMessage::DistributeAssignment(
_,
c_index,
c_indices,
)) => {
assert_eq!(candidate_index, c_index);
assert_eq!(Into::<CandidateBitfield>::into(candidate_index), c_indices);
}
);
@@ -2379,7 +2698,7 @@ async fn handle_double_assignment_import(
_,
c_index
)) => {
assert_eq!(candidate_index, c_index);
assert_eq!(Into::<CandidateBitfield>::into(candidate_index), c_index);
}
);
@@ -2400,7 +2719,6 @@ async fn handle_double_assignment_import(
overseer_recv(virtual_overseer).await,
AllMessages::ApprovalDistribution(ApprovalDistributionMessage::DistributeApproval(_))
);
// Assert that there are no more messages being sent by the subsystem
assert!(overseer_recv(virtual_overseer).timeout(TIMEOUT / 2).await.is_none());
}
@@ -2475,8 +2793,9 @@ where
let mut assignments = HashMap::new();
let _ = assignments.insert(
CoreIndex(0),
approval_db::v1::OurAssignment {
cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 }),
approval_db::v2::OurAssignment {
cert: garbage_assignment_cert(AssignmentCertKind::RelayVRFModulo { sample: 0 })
.into(),
tranche: our_assigned_tranche,
validator_index: ValidatorIndex(0),
triggered: false,
@@ -2610,14 +2929,12 @@ async fn step_until_done(clock: &MockClock) {
futures_timer::Delay::new(Duration::from_millis(200)).await;
let mut clock = clock.inner.lock();
if let Some(tick) = clock.next_wakeup() {
println!("TICK: {:?}", tick);
relevant_ticks.push(tick);
clock.set_tick(tick);
} else {
break
}
}
println!("relevant_ticks: {:?}", relevant_ticks);
}
#[test]
@@ -17,7 +17,7 @@
//! Time utilities for approval voting.
use futures::prelude::*;
use polkadot_node_primitives::approval::DelayTranche;
use polkadot_node_primitives::approval::v1::DelayTranche;
use sp_consensus_slots::Slot;
use std::{
pin::Pin,
@@ -117,7 +117,7 @@ pub enum OwnVoteState {
}
impl OwnVoteState {
fn new(votes: &CandidateVotes, env: &CandidateEnvironment<'_>) -> Self {
fn new(votes: &CandidateVotes, env: &CandidateEnvironment) -> Self {
let controlled_indices = env.controlled_indices();
if controlled_indices.is_empty() {
return Self::CannotVote
@@ -14,10 +14,12 @@ polkadot-node-subsystem-util = { path = "../../subsystem-util" }
polkadot-primitives = { path = "../../../primitives" }
polkadot-node-jaeger = { path = "../../jaeger" }
rand = "0.8"
itertools = "0.10.5"
futures = "0.3.21"
futures-timer = "3.0.2"
gum = { package = "tracing-gum", path = "../../gum" }
bitvec = { version = "1.0.0", default-features = false, features = ["alloc"] }
[dev-dependencies]
sp-authority-discovery = { path = "../../../../substrate/primitives/authority-discovery" }
File diff suppressed because it is too large Load Diff
@@ -15,6 +15,7 @@
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use polkadot_node_metrics::metrics::{prometheus, Metrics as MetricsTrait};
use polkadot_node_primitives::approval::v2::AssignmentCertKindV2;
/// Approval Distribution metrics.
#[derive(Default, Clone)]
@@ -22,21 +23,34 @@ pub struct Metrics(Option<MetricsInner>);
#[derive(Clone)]
struct MetricsInner {
assignments_imported_total: prometheus::Counter<prometheus::U64>,
assignments_imported_total: prometheus::CounterVec<prometheus::U64>,
approvals_imported_total: prometheus::Counter<prometheus::U64>,
unified_with_peer_total: prometheus::Counter<prometheus::U64>,
aggression_l1_messages_total: prometheus::Counter<prometheus::U64>,
aggression_l2_messages_total: prometheus::Counter<prometheus::U64>,
time_unify_with_peer: prometheus::Histogram,
time_import_pending_now_known: prometheus::Histogram,
time_awaiting_approval_voting: prometheus::Histogram,
}
trait AsLabel {
fn as_label(&self) -> &str;
}
impl AsLabel for &AssignmentCertKindV2 {
fn as_label(&self) -> &str {
match self {
AssignmentCertKindV2::RelayVRFDelay { .. } => "VRF Delay",
AssignmentCertKindV2::RelayVRFModulo { .. } => "VRF Modulo",
AssignmentCertKindV2::RelayVRFModuloCompact { .. } => "VRF Modulo Compact",
}
}
}
impl Metrics {
pub(crate) fn on_assignment_imported(&self) {
pub(crate) fn on_assignment_imported(&self, kind: &AssignmentCertKindV2) {
if let Some(metrics) = &self.0 {
metrics.assignments_imported_total.inc();
metrics.assignments_imported_total.with_label_values(&[kind.as_label()]).inc();
}
}
@@ -89,9 +103,12 @@ impl MetricsTrait for Metrics {
fn try_register(registry: &prometheus::Registry) -> Result<Self, prometheus::PrometheusError> {
let metrics = MetricsInner {
assignments_imported_total: prometheus::register(
prometheus::Counter::new(
"polkadot_parachain_assignments_imported_total",
"Number of valid assignments imported locally or from other peers.",
prometheus::CounterVec::new(
prometheus::Opts::new(
"polkadot_parachain_assignments_imported_total",
"Number of valid assignments imported locally or from other peers.",
),
&["kind"],
)?,
registry,
)?,
@@ -124,10 +141,16 @@ impl MetricsTrait for Metrics {
registry,
)?,
time_unify_with_peer: prometheus::register(
prometheus::Histogram::with_opts(prometheus::HistogramOpts::new(
"polkadot_parachain_time_unify_with_peer",
"Time spent within fn `unify_with_peer`.",
).buckets(vec![0.000625, 0.00125,0.0025, 0.005, 0.0075, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,]))?,
prometheus::Histogram::with_opts(
prometheus::HistogramOpts::new(
"polkadot_parachain_time_unify_with_peer",
"Time spent within fn `unify_with_peer`.",
)
.buckets(vec![
0.000625, 0.00125, 0.0025, 0.005, 0.0075, 0.01, 0.025, 0.05, 0.1, 0.25,
0.5, 1.0, 2.5, 5.0, 10.0,
]),
)?,
registry,
)?,
time_import_pending_now_known: prometheus::register(
@@ -24,20 +24,26 @@ use polkadot_node_network_protocol::{
view, ObservedRole,
};
use polkadot_node_primitives::approval::{
AssignmentCertKind, VrfOutput, VrfProof, VrfSignature, RELAY_VRF_MODULO_CONTEXT,
v1::{
AssignmentCert, AssignmentCertKind, IndirectAssignmentCert, VrfOutput, VrfProof,
VrfSignature,
},
v2::{
AssignmentCertKindV2, AssignmentCertV2, CoreBitfield, IndirectAssignmentCertV2,
RELAY_VRF_MODULO_CONTEXT,
},
};
use polkadot_node_subsystem::messages::{
network_bridge_event, AllMessages, ApprovalCheckError, ReportPeerMessage,
};
use polkadot_node_subsystem_test_helpers as test_helpers;
use polkadot_node_subsystem_util::{reputation::add_reputation, TimeoutExt as _};
use polkadot_primitives::{AuthorityDiscoveryId, BlakeTwo256, HashT};
use polkadot_primitives::{AuthorityDiscoveryId, BlakeTwo256, CoreIndex, HashT};
use polkadot_primitives_test_helpers::dummy_signature;
use rand::SeedableRng;
use sp_authority_discovery::AuthorityPair as AuthorityDiscoveryPair;
use sp_core::crypto::Pair as PairT;
use std::time::Duration;
type VirtualOverseer = test_helpers::TestSubsystemContextHandle<ApprovalDistributionMessage>;
fn test_harness<T: Future<Output = VirtualOverseer>>(
@@ -219,15 +225,15 @@ async fn setup_gossip_topology(
async fn setup_peer_with_view(
virtual_overseer: &mut VirtualOverseer,
peer_id: &PeerId,
validation_version: ValidationVersion,
view: View,
version: ValidationVersion,
) {
overseer_send(
virtual_overseer,
ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::PeerConnected(
*peer_id,
ObservedRole::Full,
validation_version.into(),
version.into(),
None,
)),
)
@@ -244,12 +250,43 @@ async fn setup_peer_with_view(
async fn send_message_from_peer(
virtual_overseer: &mut VirtualOverseer,
peer_id: &PeerId,
msg: net_protocol::ApprovalDistributionMessage,
msg: protocol_v1::ApprovalDistributionMessage,
) {
overseer_send(
virtual_overseer,
ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::PeerMessage(
*peer_id, msg,
*peer_id,
Versioned::V1(msg),
)),
)
.await;
}
async fn send_message_from_peer_v2(
virtual_overseer: &mut VirtualOverseer,
peer_id: &PeerId,
msg: protocol_v2::ApprovalDistributionMessage,
) {
overseer_send(
virtual_overseer,
ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::PeerMessage(
*peer_id,
Versioned::V2(msg),
)),
)
.await;
}
async fn send_message_from_peer_vstaging(
virtual_overseer: &mut VirtualOverseer,
peer_id: &PeerId,
msg: protocol_vstaging::ApprovalDistributionMessage,
) {
overseer_send(
virtual_overseer,
ApprovalDistributionMessage::NetworkBridgeUpdate(NetworkBridgeEvent::PeerMessage(
*peer_id,
Versioned::VStaging(msg),
)),
)
.await;
@@ -273,6 +310,28 @@ fn fake_assignment_cert(block_hash: Hash, validator: ValidatorIndex) -> Indirect
}
}
fn fake_assignment_cert_v2(
block_hash: Hash,
validator: ValidatorIndex,
core_bitfield: CoreBitfield,
) -> IndirectAssignmentCertV2 {
let ctx = schnorrkel::signing_context(RELAY_VRF_MODULO_CONTEXT);
let msg = b"WhenParachains?";
let mut prng = rand_core::OsRng;
let keypair = schnorrkel::Keypair::generate_with(&mut prng);
let (inout, proof, _) = keypair.vrf_sign(ctx.bytes(msg));
let out = inout.to_output();
IndirectAssignmentCertV2 {
block_hash,
validator,
cert: AssignmentCertV2 {
kind: AssignmentCertKindV2::RelayVRFModuloCompact { core_bitfield },
vrf: VrfSignature { output: VrfOutput(out), proof: VrfProof(proof) },
},
}
}
async fn expect_reputation_change(
virtual_overseer: &mut VirtualOverseer,
peer_id: &PeerId,
@@ -331,9 +390,9 @@ fn try_import_the_same_assignment() {
let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
let overseer = &mut virtual_overseer;
// setup peers
setup_peer_with_view(overseer, &peer_a, ValidationVersion::V1, view![]).await;
setup_peer_with_view(overseer, &peer_b, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, &peer_c, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await;
setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await;
setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V1).await;
// new block `hash_a` with 1 candidates
let meta = BlockApprovalMeta {
@@ -353,7 +412,7 @@ fn try_import_the_same_assignment() {
let assignments = vec![(cert.clone(), 0u32)];
let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
send_message_from_peer(overseer, &peer_a, Versioned::V1(msg)).await;
send_message_from_peer(overseer, &peer_a, msg).await;
expect_reputation_change(overseer, &peer_a, COST_UNEXPECTED_MESSAGE).await;
@@ -362,10 +421,11 @@ fn try_import_the_same_assignment() {
overseer_recv(overseer).await,
AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
assignment,
0u32,
claimed_indices,
tx,
)) => {
assert_eq!(assignment, cert);
assert_eq!(claimed_indices, 0u32.into());
assert_eq!(assignment, cert.into());
tx.send(AssignmentCheckResult::Accepted).unwrap();
}
);
@@ -385,12 +445,104 @@ fn try_import_the_same_assignment() {
}
);
// setup new peer
setup_peer_with_view(overseer, &peer_d, ValidationVersion::V1, view![]).await;
// setup new peer with V2
setup_peer_with_view(overseer, &peer_d, view![], ValidationVersion::VStaging).await;
// send the same assignment from peer_d
let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments);
send_message_from_peer(overseer, &peer_d, Versioned::V1(msg)).await;
send_message_from_peer(overseer, &peer_d, msg).await;
expect_reputation_change(overseer, &peer_d, COST_UNEXPECTED_MESSAGE).await;
expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE).await;
assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
virtual_overseer
});
}
/// Just like `try_import_the_same_assignment` but use `VRFModuloCompact` assignments for multiple
/// cores.
#[test]
fn try_import_the_same_assignment_v2() {
let peer_a = PeerId::random();
let peer_b = PeerId::random();
let peer_c = PeerId::random();
let peer_d = PeerId::random();
let parent_hash = Hash::repeat_byte(0xFF);
let hash = Hash::repeat_byte(0xAA);
let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
let overseer = &mut virtual_overseer;
// setup peers
setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::VStaging).await;
setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::VStaging).await;
setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::VStaging).await;
// new block `hash_a` with 1 candidates
let meta = BlockApprovalMeta {
hash,
parent_hash,
number: 2,
candidates: vec![Default::default(); 1],
slot: 1.into(),
session: 1,
};
let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
overseer_send(overseer, msg).await;
// send the assignment related to `hash`
let validator_index = ValidatorIndex(0);
let cores = vec![1, 2, 3, 4];
let core_bitfield: CoreBitfield = cores
.iter()
.map(|index| CoreIndex(*index))
.collect::<Vec<_>>()
.try_into()
.unwrap();
let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield.clone());
let assignments = vec![(cert.clone(), cores.clone().try_into().unwrap())];
let msg = protocol_vstaging::ApprovalDistributionMessage::Assignments(assignments.clone());
send_message_from_peer_vstaging(overseer, &peer_a, msg).await;
expect_reputation_change(overseer, &peer_a, COST_UNEXPECTED_MESSAGE).await;
// send an `Accept` message from the Approval Voting subsystem
assert_matches!(
overseer_recv(overseer).await,
AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
assignment,
claimed_indices,
tx,
)) => {
assert_eq!(claimed_indices, cores.try_into().unwrap());
assert_eq!(assignment, cert.into());
tx.send(AssignmentCheckResult::Accepted).unwrap();
}
);
expect_reputation_change(overseer, &peer_a, BENEFIT_VALID_MESSAGE_FIRST).await;
assert_matches!(
overseer_recv(overseer).await,
AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
peers,
Versioned::VStaging(protocol_vstaging::ValidationProtocol::ApprovalDistribution(
protocol_vstaging::ApprovalDistributionMessage::Assignments(assignments)
))
)) => {
assert_eq!(peers.len(), 2);
assert_eq!(assignments.len(), 1);
}
);
// setup new peer
setup_peer_with_view(overseer, &peer_d, view![], ValidationVersion::VStaging).await;
// send the same assignment from peer_d
let msg = protocol_vstaging::ApprovalDistributionMessage::Assignments(assignments);
send_message_from_peer_vstaging(overseer, &peer_d, msg).await;
expect_reputation_change(overseer, &peer_d, COST_UNEXPECTED_MESSAGE).await;
expect_reputation_change(overseer, &peer_d, BENEFIT_VALID_MESSAGE).await;
@@ -413,7 +565,7 @@ fn delay_reputation_change() {
let overseer = &mut virtual_overseer;
// Setup peers
setup_peer_with_view(overseer, &peer, ValidationVersion::V1, view![]).await;
setup_peer_with_view(overseer, &peer, view![], ValidationVersion::V1).await;
// new block `hash_a` with 1 candidates
let meta = BlockApprovalMeta {
@@ -433,17 +585,18 @@ fn delay_reputation_change() {
let assignments = vec![(cert.clone(), 0u32)];
let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
send_message_from_peer(overseer, &peer, Versioned::V1(msg)).await;
send_message_from_peer(overseer, &peer, msg).await;
// send an `Accept` message from the Approval Voting subsystem
assert_matches!(
overseer_recv(overseer).await,
AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
assignment,
0u32,
claimed_candidates,
tx,
)) => {
assert_eq!(assignment, cert);
assert_eq!(assignment.cert, cert.cert.into());
assert_eq!(claimed_candidates, vec![0u32].try_into().unwrap());
tx.send(AssignmentCheckResult::Accepted).unwrap();
}
);
@@ -474,7 +627,7 @@ fn spam_attack_results_in_negative_reputation_change() {
let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
let overseer = &mut virtual_overseer;
let peer = &peer_a;
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![]).await;
setup_peer_with_view(overseer, peer, view![], ValidationVersion::V1).await;
// new block `hash_b` with 20 candidates
let candidates_count = 20;
@@ -501,7 +654,7 @@ fn spam_attack_results_in_negative_reputation_change() {
.collect();
let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
send_message_from_peer(overseer, peer, Versioned::V1(msg.clone())).await;
send_message_from_peer(overseer, peer, msg.clone()).await;
for i in 0..candidates_count {
expect_reputation_change(overseer, peer, COST_UNEXPECTED_MESSAGE).await;
@@ -513,8 +666,8 @@ fn spam_attack_results_in_negative_reputation_change() {
claimed_candidate_index,
tx,
)) => {
assert_eq!(assignment, assignments[i].0);
assert_eq!(claimed_candidate_index, assignments[i].1);
assert_eq!(assignment, assignments[i].0.clone().into());
assert_eq!(claimed_candidate_index, assignments[i].1.into());
tx.send(AssignmentCheckResult::Accepted).unwrap();
}
);
@@ -533,7 +686,7 @@ fn spam_attack_results_in_negative_reputation_change() {
.await;
// send the assignments again
send_message_from_peer(overseer, peer, Versioned::V1(msg.clone())).await;
send_message_from_peer(overseer, peer, msg.clone()).await;
// each of them will incur `COST_UNEXPECTED_MESSAGE`, not only the first one
for _ in 0..candidates_count {
@@ -558,7 +711,7 @@ fn peer_sending_us_the_same_we_just_sent_them_is_ok() {
let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
let overseer = &mut virtual_overseer;
let peer = &peer_a;
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![]).await;
setup_peer_with_view(overseer, peer, view![], ValidationVersion::V1).await;
// new block `hash` with 1 candidates
let meta = BlockApprovalMeta {
@@ -578,7 +731,10 @@ fn peer_sending_us_the_same_we_just_sent_them_is_ok() {
let cert = fake_assignment_cert(hash, validator_index);
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(cert.clone(), candidate_index),
ApprovalDistributionMessage::DistributeAssignment(
cert.clone().into(),
candidate_index.into(),
),
)
.await;
@@ -610,12 +766,12 @@ fn peer_sending_us_the_same_we_just_sent_them_is_ok() {
// the peer could send us it as well
let assignments = vec![(cert, candidate_index)];
let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments);
send_message_from_peer(overseer, peer, Versioned::V1(msg.clone())).await;
send_message_from_peer(overseer, peer, msg.clone()).await;
assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "we should not punish the peer");
// send the assignments again
send_message_from_peer(overseer, peer, Versioned::V1(msg)).await;
send_message_from_peer(overseer, peer, msg).await;
// now we should
expect_reputation_change(overseer, peer, COST_DUPLICATE_MESSAGE).await;
@@ -633,10 +789,10 @@ fn import_approval_happy_path() {
let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
let overseer = &mut virtual_overseer;
// setup peers
setup_peer_with_view(overseer, &peer_a, ValidationVersion::V1, view![]).await;
setup_peer_with_view(overseer, &peer_b, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, &peer_c, ValidationVersion::V1, view![hash]).await;
// setup peers with V1 and V2 protocol versions
setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await;
setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::VStaging).await;
setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V1).await;
// new block `hash_a` with 1 candidates
let meta = BlockApprovalMeta {
@@ -656,10 +812,14 @@ fn import_approval_happy_path() {
let cert = fake_assignment_cert(hash, validator_index);
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(cert, candidate_index),
ApprovalDistributionMessage::DistributeAssignment(
cert.clone().into(),
candidate_index.into(),
),
)
.await;
// 1 peer is v1
assert_matches!(
overseer_recv(overseer).await,
AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
@@ -668,7 +828,21 @@ fn import_approval_happy_path() {
protocol_v1::ApprovalDistributionMessage::Assignments(assignments)
))
)) => {
assert_eq!(peers.len(), 2);
assert_eq!(peers.len(), 1);
assert_eq!(assignments.len(), 1);
}
);
// 1 peer is v2
assert_matches!(
overseer_recv(overseer).await,
AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
peers,
Versioned::VStaging(protocol_vstaging::ValidationProtocol::ApprovalDistribution(
protocol_vstaging::ApprovalDistributionMessage::Assignments(assignments)
))
)) => {
assert_eq!(peers.len(), 1);
assert_eq!(assignments.len(), 1);
}
);
@@ -681,7 +855,7 @@ fn import_approval_happy_path() {
signature: dummy_signature(),
};
let msg = protocol_v1::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
send_message_from_peer(overseer, &peer_b, Versioned::V1(msg)).await;
send_message_from_peer(overseer, &peer_b, msg).await;
assert_matches!(
overseer_recv(overseer).await,
@@ -722,8 +896,8 @@ fn import_approval_bad() {
let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
let overseer = &mut virtual_overseer;
// setup peers
setup_peer_with_view(overseer, &peer_a, ValidationVersion::V1, view![]).await;
setup_peer_with_view(overseer, &peer_b, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, &peer_a, view![], ValidationVersion::V1).await;
setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await;
// new block `hash_a` with 1 candidates
let meta = BlockApprovalMeta {
@@ -749,14 +923,14 @@ fn import_approval_bad() {
signature: dummy_signature(),
};
let msg = protocol_v1::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
send_message_from_peer(overseer, &peer_b, Versioned::V1(msg)).await;
send_message_from_peer(overseer, &peer_b, msg).await;
expect_reputation_change(overseer, &peer_b, COST_UNEXPECTED_MESSAGE).await;
// now import an assignment from peer_b
let assignments = vec![(cert.clone(), candidate_index)];
let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments);
send_message_from_peer(overseer, &peer_b, Versioned::V1(msg)).await;
send_message_from_peer(overseer, &peer_b, msg).await;
assert_matches!(
overseer_recv(overseer).await,
@@ -765,8 +939,8 @@ fn import_approval_bad() {
i,
tx,
)) => {
assert_eq!(assignment, cert);
assert_eq!(i, candidate_index);
assert_eq!(assignment, cert.into());
assert_eq!(i, candidate_index.into());
tx.send(AssignmentCheckResult::Accepted).unwrap();
}
);
@@ -775,7 +949,7 @@ fn import_approval_bad() {
// and try again
let msg = protocol_v1::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
send_message_from_peer(overseer, &peer_b, Versioned::V1(msg)).await;
send_message_from_peer(overseer, &peer_b, msg).await;
assert_matches!(
overseer_recv(overseer).await,
@@ -911,12 +1085,20 @@ fn update_peer_view() {
let cert_a = fake_assignment_cert(hash_a, ValidatorIndex(0));
let cert_b = fake_assignment_cert(hash_b, ValidatorIndex(0));
overseer_send(overseer, ApprovalDistributionMessage::DistributeAssignment(cert_a, 0)).await;
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(cert_a.into(), 0.into()),
)
.await;
overseer_send(overseer, ApprovalDistributionMessage::DistributeAssignment(cert_b, 0)).await;
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(cert_b.into(), 0.into()),
)
.await;
// connect a peer
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![hash_a]).await;
setup_peer_with_view(overseer, peer, view![hash_a], ValidationVersion::V1).await;
// we should send relevant assignments to the peer
assert_matches!(
@@ -934,7 +1116,7 @@ fn update_peer_view() {
virtual_overseer
});
assert_eq!(state.peer_data.get(peer).map(|data| data.view.finalized_number), Some(0));
assert_eq!(state.peer_views.get(peer).map(|v| v.view.finalized_number), Some(0));
assert_eq!(
state
.blocks
@@ -965,7 +1147,7 @@ fn update_peer_view() {
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(cert_c.clone(), 0),
ApprovalDistributionMessage::DistributeAssignment(cert_c.clone().into(), 0.into()),
)
.await;
@@ -986,7 +1168,7 @@ fn update_peer_view() {
virtual_overseer
});
assert_eq!(state.peer_data.get(peer).map(|data| data.view.finalized_number), Some(2));
assert_eq!(state.peer_views.get(peer).map(|v| v.view.finalized_number), Some(2));
assert_eq!(
state
.blocks
@@ -1016,10 +1198,7 @@ fn update_peer_view() {
virtual_overseer
});
assert_eq!(
state.peer_data.get(peer).map(|data| data.view.finalized_number),
Some(finalized_number)
);
assert_eq!(state.peer_views.get(peer).map(|v| v.view.finalized_number), Some(finalized_number));
assert!(state.blocks.get(&hash_c).unwrap().known_by.get(peer).is_none());
}
@@ -1034,7 +1213,7 @@ fn import_remotely_then_locally() {
let _ = test_harness(state_without_reputation_delay(), |mut virtual_overseer| async move {
let overseer = &mut virtual_overseer;
// setup the peer
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
// new block `hash_a` with 1 candidates
let meta = BlockApprovalMeta {
@@ -1054,7 +1233,7 @@ fn import_remotely_then_locally() {
let cert = fake_assignment_cert(hash, validator_index);
let assignments = vec![(cert.clone(), candidate_index)];
let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
send_message_from_peer(overseer, peer, Versioned::V1(msg)).await;
send_message_from_peer(overseer, peer, msg).await;
// send an `Accept` message from the Approval Voting subsystem
assert_matches!(
@@ -1064,8 +1243,8 @@ fn import_remotely_then_locally() {
i,
tx,
)) => {
assert_eq!(assignment, cert);
assert_eq!(i, candidate_index);
assert_eq!(assignment, cert.clone().into());
assert_eq!(i, candidate_index.into());
tx.send(AssignmentCheckResult::Accepted).unwrap();
}
);
@@ -1075,7 +1254,10 @@ fn import_remotely_then_locally() {
// import the same assignment locally
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(cert, candidate_index),
ApprovalDistributionMessage::DistributeAssignment(
cert.clone().into(),
candidate_index.into(),
),
)
.await;
@@ -1089,7 +1271,7 @@ fn import_remotely_then_locally() {
signature: dummy_signature(),
};
let msg = protocol_v1::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
send_message_from_peer(overseer, peer, Versioned::V1(msg)).await;
send_message_from_peer(overseer, peer, msg).await;
assert_matches!(
overseer_recv(overseer).await,
@@ -1147,7 +1329,10 @@ fn sends_assignments_even_when_state_is_approved() {
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(cert.clone(), candidate_index),
ApprovalDistributionMessage::DistributeAssignment(
cert.clone().into(),
candidate_index.into(),
),
)
.await;
@@ -1155,7 +1340,7 @@ fn sends_assignments_even_when_state_is_approved() {
.await;
// connect the peer.
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
let assignments = vec![(cert.clone(), candidate_index)];
let approvals = vec![approval.clone()];
@@ -1191,6 +1376,112 @@ fn sends_assignments_even_when_state_is_approved() {
});
}
/// Same as `sends_assignments_even_when_state_is_approved_v2` but with `VRFModuloCompact`
/// assignemnts.
#[test]
fn sends_assignments_even_when_state_is_approved_v2() {
let peer_a = PeerId::random();
let parent_hash = Hash::repeat_byte(0xFF);
let hash = Hash::repeat_byte(0xAA);
let peer = &peer_a;
let _ = test_harness(State::default(), |mut virtual_overseer| async move {
let overseer = &mut virtual_overseer;
// new block `hash_a` with 1 candidates
let meta = BlockApprovalMeta {
hash,
parent_hash,
number: 1,
candidates: vec![Default::default(); 4],
slot: 1.into(),
session: 1,
};
let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
overseer_send(overseer, msg).await;
let validator_index = ValidatorIndex(0);
let cores = vec![0, 1, 2, 3];
let candidate_bitfield: CandidateBitfield = cores.clone().try_into().unwrap();
let core_bitfield: CoreBitfield = cores
.iter()
.map(|index| CoreIndex(*index))
.collect::<Vec<_>>()
.try_into()
.unwrap();
let cert = fake_assignment_cert_v2(hash, validator_index, core_bitfield.clone());
// Assumes candidate index == core index.
let approvals = cores
.iter()
.map(|core| IndirectSignedApprovalVote {
block_hash: hash,
candidate_index: *core,
validator: validator_index,
signature: dummy_signature(),
})
.collect::<Vec<_>>();
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(
cert.clone().into(),
candidate_bitfield.clone(),
),
)
.await;
for approval in &approvals {
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeApproval(approval.clone()),
)
.await;
}
// connect the peer.
setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::VStaging).await;
let assignments = vec![(cert.clone(), candidate_bitfield.clone())];
assert_matches!(
overseer_recv(overseer).await,
AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
peers,
Versioned::VStaging(protocol_vstaging::ValidationProtocol::ApprovalDistribution(
protocol_vstaging::ApprovalDistributionMessage::Assignments(sent_assignments)
))
)) => {
assert_eq!(peers, vec![*peer]);
assert_eq!(sent_assignments, assignments);
}
);
assert_matches!(
overseer_recv(overseer).await,
AllMessages::NetworkBridgeTx(NetworkBridgeTxMessage::SendValidationMessage(
peers,
Versioned::VStaging(protocol_vstaging::ValidationProtocol::ApprovalDistribution(
protocol_vstaging::ApprovalDistributionMessage::Approvals(sent_approvals)
))
)) => {
// Construct a hashmaps of approvals for comparison. Approval distribution reorders messages because they are kept in a
// hashmap as well.
let sent_approvals = sent_approvals.into_iter().map(|approval| (approval.candidate_index, approval)).collect::<HashMap<_,_>>();
let approvals = approvals.into_iter().map(|approval| (approval.candidate_index, approval)).collect::<HashMap<_,_>>();
assert_eq!(peers, vec![*peer]);
assert_eq!(sent_approvals, approvals);
}
);
assert!(overseer.recv().timeout(TIMEOUT).await.is_none(), "no message should be sent");
virtual_overseer
});
}
/// <https://github.com/paritytech/polkadot/pull/5089>
///
/// 1. Receive remote peer view update with an unknown head
@@ -1219,7 +1510,7 @@ fn race_condition_in_local_vs_remote_view_update() {
};
// This will send a peer view that is ahead of our view
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![hash_b]).await;
setup_peer_with_view(overseer, peer, view![hash_b], ValidationVersion::V1).await;
// Send our view update to include a new head
overseer_send(
@@ -1240,7 +1531,7 @@ fn race_condition_in_local_vs_remote_view_update() {
.collect();
let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
send_message_from_peer(overseer, peer, Versioned::V1(msg.clone())).await;
send_message_from_peer(overseer, peer, msg.clone()).await;
// This will handle pending messages being processed
let msg = ApprovalDistributionMessage::NewBlocks(vec![meta]);
@@ -1257,8 +1548,8 @@ fn race_condition_in_local_vs_remote_view_update() {
claimed_candidate_index,
tx,
)) => {
assert_eq!(assignment, assignments[i].0);
assert_eq!(claimed_candidate_index, assignments[i].1);
assert_eq!(assignment, assignments[i].0.clone().into());
assert_eq!(claimed_candidate_index, assignments[i].1.into());
tx.send(AssignmentCheckResult::Accepted).unwrap();
}
);
@@ -1283,7 +1574,7 @@ fn propagates_locally_generated_assignment_to_both_dimensions() {
// Connect all peers.
for (peer, _) in &peers {
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
}
// Set up a gossip topology.
@@ -1325,7 +1616,10 @@ fn propagates_locally_generated_assignment_to_both_dimensions() {
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(cert.clone(), candidate_index),
ApprovalDistributionMessage::DistributeAssignment(
cert.clone().into(),
candidate_index.into(),
),
)
.await;
@@ -1388,7 +1682,7 @@ fn propagates_assignments_along_unshared_dimension() {
// Connect all peers.
for (peer, _) in &peers {
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
}
// Set up a gossip topology.
@@ -1424,7 +1718,7 @@ fn propagates_assignments_along_unshared_dimension() {
// Issuer of the message is important, not the peer we receive from.
// 99 deliberately chosen because it's not in X or Y.
send_message_from_peer(overseer, &peers[99].0, Versioned::V1(msg)).await;
send_message_from_peer(overseer, &peers[99].0, msg).await;
assert_matches!(
overseer_recv(overseer).await,
AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
@@ -1473,7 +1767,7 @@ fn propagates_assignments_along_unshared_dimension() {
// Issuer of the message is important, not the peer we receive from.
// 99 deliberately chosen because it's not in X or Y.
send_message_from_peer(overseer, &peers[99].0, Versioned::V1(msg)).await;
send_message_from_peer(overseer, &peers[99].0, msg).await;
assert_matches!(
overseer_recv(overseer).await,
AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
@@ -1530,7 +1824,7 @@ fn propagates_to_required_after_connect() {
// Connect all peers except omitted.
for (i, (peer, _)) in peers.iter().enumerate() {
if !omitted.contains(&i) {
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
}
}
@@ -1573,7 +1867,10 @@ fn propagates_to_required_after_connect() {
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(cert.clone(), candidate_index),
ApprovalDistributionMessage::DistributeAssignment(
cert.clone().into(),
candidate_index.into(),
),
)
.await;
@@ -1619,7 +1916,7 @@ fn propagates_to_required_after_connect() {
);
for i in omitted.iter().copied() {
setup_peer_with_view(overseer, &peers[i].0, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, &peers[i].0, view![hash], ValidationVersion::V1).await;
assert_matches!(
overseer_recv(overseer).await,
@@ -1668,7 +1965,7 @@ fn sends_to_more_peers_after_getting_topology() {
// Connect all peers except omitted.
for (peer, _) in &peers {
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
}
// new block `hash_a` with 1 candidates
@@ -1698,7 +1995,10 @@ fn sends_to_more_peers_after_getting_topology() {
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(cert.clone(), candidate_index),
ApprovalDistributionMessage::DistributeAssignment(
cert.clone().into(),
candidate_index.into(),
),
)
.await;
@@ -1820,7 +2120,7 @@ fn originator_aggression_l1() {
// Connect all peers except omitted.
for (peer, _) in &peers {
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
}
// new block `hash_a` with 1 candidates
@@ -1857,7 +2157,10 @@ fn originator_aggression_l1() {
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(cert.clone(), candidate_index),
ApprovalDistributionMessage::DistributeAssignment(
cert.clone().into(),
candidate_index.into(),
),
)
.await;
@@ -1979,7 +2282,7 @@ fn non_originator_aggression_l1() {
// Connect all peers except omitted.
for (peer, _) in &peers {
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
}
// new block `hash_a` with 1 candidates
@@ -2008,12 +2311,12 @@ fn non_originator_aggression_l1() {
)
.await;
let assignments = vec![(cert.clone(), candidate_index)];
let assignments = vec![(cert.clone().into(), candidate_index)];
let msg = protocol_v1::ApprovalDistributionMessage::Assignments(assignments.clone());
// Issuer of the message is important, not the peer we receive from.
// 99 deliberately chosen because it's not in X or Y.
send_message_from_peer(overseer, &peers[99].0, Versioned::V1(msg)).await;
send_message_from_peer(overseer, &peers[99].0, msg).await;
assert_matches!(
overseer_recv(overseer).await,
AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
@@ -2084,7 +2387,7 @@ fn non_originator_aggression_l2() {
// Connect all peers except omitted.
for (peer, _) in &peers {
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
}
// new block `hash_a` with 1 candidates
@@ -2118,7 +2421,7 @@ fn non_originator_aggression_l2() {
// Issuer of the message is important, not the peer we receive from.
// 99 deliberately chosen because it's not in X or Y.
send_message_from_peer(overseer, &peers[99].0, Versioned::V1(msg)).await;
send_message_from_peer(overseer, &peers[99].0, msg).await;
assert_matches!(
overseer_recv(overseer).await,
AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
@@ -2249,7 +2552,7 @@ fn resends_messages_periodically() {
// Connect all peers.
for (peer, _) in &peers {
setup_peer_with_view(overseer, peer, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, peer, view![hash], ValidationVersion::V1).await;
}
// Set up a gossip topology.
@@ -2284,7 +2587,7 @@ fn resends_messages_periodically() {
// Issuer of the message is important, not the peer we receive from.
// 99 deliberately chosen because it's not in X or Y.
send_message_from_peer(overseer, &peers[99].0, Versioned::V1(msg)).await;
send_message_from_peer(overseer, &peers[99].0, msg).await;
assert_matches!(
overseer_recv(overseer).await,
AllMessages::ApprovalVoting(ApprovalVotingMessage::CheckAndImportAssignment(
@@ -2388,9 +2691,9 @@ fn import_versioned_approval() {
let _ = test_harness(state, |mut virtual_overseer| async move {
let overseer = &mut virtual_overseer;
// All peers are aware of relay parent.
setup_peer_with_view(overseer, &peer_a, ValidationVersion::V2, view![hash]).await;
setup_peer_with_view(overseer, &peer_b, ValidationVersion::V1, view![hash]).await;
setup_peer_with_view(overseer, &peer_c, ValidationVersion::V2, view![hash]).await;
setup_peer_with_view(overseer, &peer_a, view![hash], ValidationVersion::V2).await;
setup_peer_with_view(overseer, &peer_b, view![hash], ValidationVersion::V1).await;
setup_peer_with_view(overseer, &peer_c, view![hash], ValidationVersion::V2).await;
// new block `hash_a` with 1 candidates
let meta = BlockApprovalMeta {
@@ -2410,7 +2713,7 @@ fn import_versioned_approval() {
let cert = fake_assignment_cert(hash, validator_index);
overseer_send(
overseer,
ApprovalDistributionMessage::DistributeAssignment(cert, candidate_index),
ApprovalDistributionMessage::DistributeAssignment(cert.into(), candidate_index.into()),
)
.await;
@@ -2451,7 +2754,7 @@ fn import_versioned_approval() {
signature: dummy_signature(),
};
let msg = protocol_v2::ApprovalDistributionMessage::Approvals(vec![approval.clone()]);
send_message_from_peer(overseer, &peer_a, Versioned::V2(msg)).await;
send_message_from_peer_v2(overseer, &peer_a, msg).await;
assert_matches!(
overseer_recv(overseer).await,
@@ -2512,7 +2815,9 @@ fn batch_test_round(message_count: usize) {
let validators = 0..message_count;
let assignments: Vec<_> = validators
.clone()
.map(|index| (fake_assignment_cert(Hash::zero(), ValidatorIndex(index as u32)), 0))
.map(|index| {
(fake_assignment_cert(Hash::zero(), ValidatorIndex(index as u32)).into(), 0.into())
})
.collect();
let approvals: Vec<_> = validators
@@ -2525,9 +2830,18 @@ fn batch_test_round(message_count: usize) {
.collect();
let peer = PeerId::random();
send_assignments_batched(&mut sender, assignments.clone(), peer, ValidationVersion::V1)
.await;
send_approvals_batched(&mut sender, approvals.clone(), peer, ValidationVersion::V1).await;
send_assignments_batched(
&mut sender,
assignments.clone(),
&vec![(peer, ValidationVersion::V1.into())],
)
.await;
send_approvals_batched(
&mut sender,
approvals.clone(),
&vec![(peer, ValidationVersion::V1.into())],
)
.await;
// Check expected assignments batches.
for assignment_index in (0..assignments.len()).step_by(super::MAX_ASSIGNMENT_BATCH_SIZE) {
@@ -2549,7 +2863,7 @@ fn batch_test_round(message_count: usize) {
assert_eq!(peers.len(), 1);
for (message_index, assignment) in sent_assignments.iter().enumerate() {
assert_eq!(assignment.0, assignments[assignment_index + message_index].0);
assert_eq!(assignment.0, assignments[assignment_index + message_index].0.clone().try_into().unwrap());
assert_eq!(assignment.1, 0);
}
}
@@ -25,14 +25,15 @@
use always_assert::never;
use futures::{channel::oneshot, FutureExt};
use net_protocol::filter_by_peer_version;
use polkadot_node_network_protocol::{
self as net_protocol,
grid_topology::{
GridNeighbors, RandomRouting, RequiredRouting, SessionBoundGridTopologyStorage,
},
peer_set::{ProtocolVersion, ValidationVersion},
v1 as protocol_v1, v2 as protocol_v2, OurView, PeerId, UnifiedReputationChange as Rep,
Versioned, View,
v1 as protocol_v1, v2 as protocol_v2, vstaging as protocol_vstaging, OurView, PeerId,
UnifiedReputationChange as Rep, Versioned, View,
};
use polkadot_node_subsystem::{
jaeger, messages::*, overseer, ActiveLeavesUpdate, FromOrchestra, OverseerSignal, PerLeafSpan,
@@ -101,6 +102,11 @@ impl BitfieldGossipMessage {
self.relay_parent,
self.signed_availability.into(),
)),
Some(ValidationVersion::VStaging) =>
Versioned::VStaging(protocol_vstaging::BitfieldDistributionMessage::Bitfield(
self.relay_parent,
self.signed_availability.into(),
)),
None => {
never!("Peers should only have supported protocol versions.");
@@ -131,9 +137,9 @@ pub struct PeerData {
/// Data used to track information of peers and relay parents the
/// overseer ordered us to work on.
#[derive(Default, Debug)]
#[derive(Default)]
struct ProtocolState {
/// Track all active peers and their views
/// Track all active peer views and protocol versions
/// to determine what is relevant to them.
peer_data: HashMap<PeerId, PeerData>,
@@ -492,17 +498,13 @@ async fn relay_message<Context>(
} else {
let _span = span.child("gossip");
let filter_by_version = |peers: &[(PeerId, ProtocolVersion)],
version: ValidationVersion| {
peers
.iter()
.filter(|(_, v)| v == &version.into())
.map(|(peer_id, _)| *peer_id)
.collect::<Vec<_>>()
};
let v1_interested_peers =
filter_by_peer_version(&interested_peers, ValidationVersion::V1.into());
let v2_interested_peers =
filter_by_peer_version(&interested_peers, ValidationVersion::V2.into());
let v1_interested_peers = filter_by_version(&interested_peers, ValidationVersion::V1);
let v2_interested_peers = filter_by_version(&interested_peers, ValidationVersion::V2);
let vstaging_interested_peers =
filter_by_peer_version(&interested_peers, ValidationVersion::VStaging.into());
if !v1_interested_peers.is_empty() {
ctx.send_message(NetworkBridgeTxMessage::SendValidationMessage(
@@ -515,7 +517,15 @@ async fn relay_message<Context>(
if !v2_interested_peers.is_empty() {
ctx.send_message(NetworkBridgeTxMessage::SendValidationMessage(
v2_interested_peers,
message.into_validation_protocol(ValidationVersion::V2.into()),
message.clone().into_validation_protocol(ValidationVersion::V2.into()),
))
.await
}
if !vstaging_interested_peers.is_empty() {
ctx.send_message(NetworkBridgeTxMessage::SendValidationMessage(
vstaging_interested_peers,
message.into_validation_protocol(ValidationVersion::VStaging.into()),
))
.await
}
@@ -540,6 +550,10 @@ async fn process_incoming_peer_message<Context>(
Versioned::V2(protocol_v2::BitfieldDistributionMessage::Bitfield(
relay_parent,
bitfield,
)) |
Versioned::VStaging(protocol_vstaging::BitfieldDistributionMessage::Bitfield(
relay_parent,
bitfield,
)) => (relay_parent, bitfield),
};
@@ -774,9 +788,11 @@ async fn handle_network_msg<Context>(
handle_peer_view_change(ctx, state, new_peer, old_view, rng).await;
}
},
NetworkBridgeEvent::PeerViewChange(peerid, new_view) => {
gum::trace!(target: LOG_TARGET, ?peerid, ?new_view, "Peer view change");
handle_peer_view_change(ctx, state, peerid, new_view, rng).await;
NetworkBridgeEvent::PeerViewChange(peer_id, new_view) => {
gum::trace!(target: LOG_TARGET, ?peer_id, ?new_view, "Peer view change");
if state.peer_data.get(&peer_id).is_some() {
handle_peer_view_change(ctx, state, peer_id, new_view, rng).await;
}
},
NetworkBridgeEvent::OurViewChange(new_view) => {
gum::trace!(target: LOG_TARGET, ?new_view, "Our view change");
+122 -5
View File
@@ -28,23 +28,129 @@ use sc_network::{
};
use polkadot_node_network_protocol::{
peer_set::{PeerSet, PeerSetProtocolNames, ProtocolVersion},
peer_set::{
CollationVersion, PeerSet, PeerSetProtocolNames, ProtocolVersion, ValidationVersion,
},
request_response::{OutgoingRequest, Recipient, ReqProtocolNames, Requests},
PeerId,
v1 as protocol_v1, v2 as protocol_v2, vstaging as protocol_vstaging, PeerId,
};
use polkadot_primitives::{AuthorityDiscoveryId, Block, Hash};
use crate::validator_discovery::AuthorityDiscovery;
use crate::{metrics::Metrics, validator_discovery::AuthorityDiscovery, WireMessage};
// network bridge network abstraction log target
const LOG_TARGET: &'static str = "parachain::network-bridge-net";
/// Send a message to the network.
// Helper function to send a validation v1 message to a list of peers.
// Messages are always sent via the main protocol, even legacy protocol messages.
pub(crate) fn send_validation_message_v1(
net: &mut impl Network,
peers: Vec<PeerId>,
peerset_protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_v1::ValidationProtocol>,
metrics: &Metrics,
) {
gum::trace!(target: LOG_TARGET, ?peers, ?message, "Sending validation v1 message to peers",);
send_message(
net,
peers,
PeerSet::Validation,
ValidationVersion::V1.into(),
peerset_protocol_names,
message,
metrics,
);
}
// Helper function to send a validation vstaging message to a list of peers.
// Messages are always sent via the main protocol, even legacy protocol messages.
pub(crate) fn send_validation_message_vstaging(
net: &mut impl Network,
peers: Vec<PeerId>,
peerset_protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_vstaging::ValidationProtocol>,
metrics: &Metrics,
) {
gum::trace!(target: LOG_TARGET, ?peers, ?message, "Sending validation vstaging message to peers",);
send_message(
net,
peers,
PeerSet::Validation,
ValidationVersion::VStaging.into(),
peerset_protocol_names,
message,
metrics,
);
}
// Helper function to send a validation v2 message to a list of peers.
// Messages are always sent via the main protocol, even legacy protocol messages.
pub(crate) fn send_validation_message_v2(
net: &mut impl Network,
peers: Vec<PeerId>,
protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_v2::ValidationProtocol>,
metrics: &Metrics,
) {
send_message(
net,
peers,
PeerSet::Validation,
ValidationVersion::V2.into(),
protocol_names,
message,
metrics,
);
}
// Helper function to send a collation v1 message to a list of peers.
// Messages are always sent via the main protocol, even legacy protocol messages.
pub(crate) fn send_collation_message_v1(
net: &mut impl Network,
peers: Vec<PeerId>,
peerset_protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_v1::CollationProtocol>,
metrics: &Metrics,
) {
send_message(
net,
peers,
PeerSet::Collation,
CollationVersion::V1.into(),
peerset_protocol_names,
message,
metrics,
);
}
// Helper function to send a collation v2 message to a list of peers.
// Messages are always sent via the main protocol, even legacy protocol messages.
pub(crate) fn send_collation_message_v2(
net: &mut impl Network,
peers: Vec<PeerId>,
peerset_protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_v2::CollationProtocol>,
metrics: &Metrics,
) {
send_message(
net,
peers,
PeerSet::Collation,
CollationVersion::V2.into(),
peerset_protocol_names,
message,
metrics,
);
}
/// Lower level function that sends a message to the network using the main protocol version.
///
/// This function is only used internally by the network-bridge, which is responsible to only send
/// messages that are compatible with the passed peer set, as that is currently not enforced by
/// this function. These are messages of type `WireMessage` parameterized on the matching type.
pub(crate) fn send_message<M>(
fn send_message<M>(
net: &mut impl Network,
mut peers: Vec<PeerId>,
peer_set: PeerSet,
@@ -65,6 +171,17 @@ pub(crate) fn send_message<M>(
encoded
};
// optimization: generate the protocol name once.
let protocol_name = protocol_names.get_name(peer_set, version);
gum::trace!(
target: LOG_TARGET,
?peers,
?version,
?protocol_name,
?message,
"Sending message to peers",
);
// optimization: avoid cloning the message for the last peer in the
// list. The message payload can be quite large. If the underlying
// network used `Bytes` this would not be necessary.
+57 -98
View File
@@ -21,6 +21,7 @@ use super::*;
use always_assert::never;
use bytes::Bytes;
use futures::stream::{BoxStream, StreamExt};
use net_protocol::filter_by_peer_version;
use parity_scale_codec::{Decode, DecodeAll};
use sc_network::Event as NetworkEvent;
@@ -33,8 +34,8 @@ use polkadot_node_network_protocol::{
CollationVersion, PeerSet, PeerSetProtocolNames, PerPeerSet, ProtocolVersion,
ValidationVersion,
},
v1 as protocol_v1, v2 as protocol_v2, ObservedRole, OurView, PeerId,
UnifiedReputationChange as Rep, View,
v1 as protocol_v1, v2 as protocol_v2, vstaging as protocol_vstaging, ObservedRole, OurView,
PeerId, UnifiedReputationChange as Rep, View,
};
use polkadot_node_subsystem::{
@@ -64,9 +65,11 @@ use super::validator_discovery;
/// Actual interfacing to the network based on the `Network` trait.
///
/// Defines the `Network` trait with an implementation for an `Arc<NetworkService>`.
use crate::network::{send_message, Network};
use crate::network::get_peer_id_by_authority_id;
use crate::network::{
send_collation_message_v1, send_collation_message_v2, send_validation_message_v1,
send_validation_message_v2, send_validation_message_vstaging, Network,
};
use crate::{network::get_peer_id_by_authority_id, WireMessage};
use super::metrics::Metrics;
@@ -251,22 +254,27 @@ where
match ValidationVersion::try_from(version)
.expect("try_get_protocol has already checked version is known; qed")
{
ValidationVersion::V1 => send_message(
ValidationVersion::V1 => send_validation_message_v1(
&mut network_service,
vec![peer],
PeerSet::Validation,
version,
&peerset_protocol_names,
WireMessage::<protocol_v1::ValidationProtocol>::ViewUpdate(
local_view,
),
&metrics,
),
ValidationVersion::V2 => send_message(
ValidationVersion::VStaging => send_validation_message_vstaging(
&mut network_service,
vec![peer],
&peerset_protocol_names,
WireMessage::<protocol_vstaging::ValidationProtocol>::ViewUpdate(
local_view,
),
&metrics,
),
ValidationVersion::V2 => send_validation_message_v2(
&mut network_service,
vec![peer],
PeerSet::Validation,
version,
&peerset_protocol_names,
WireMessage::<protocol_v2::ValidationProtocol>::ViewUpdate(
local_view,
@@ -293,22 +301,18 @@ where
match CollationVersion::try_from(version)
.expect("try_get_protocol has already checked version is known; qed")
{
CollationVersion::V1 => send_message(
CollationVersion::V1 => send_collation_message_v1(
&mut network_service,
vec![peer],
PeerSet::Collation,
version,
&peerset_protocol_names,
WireMessage::<protocol_v1::CollationProtocol>::ViewUpdate(
local_view,
),
&metrics,
),
CollationVersion::V2 => send_message(
CollationVersion::V2 => send_collation_message_v2(
&mut network_service,
vec![peer],
PeerSet::Collation,
version,
&peerset_protocol_names,
WireMessage::<protocol_v2::CollationProtocol>::ViewUpdate(
local_view,
@@ -386,8 +390,16 @@ where
.filter_map(|(protocol, msg_bytes)| {
// version doesn't matter because we always receive on the 'correct'
// protocol name, not the negotiated fallback.
let (peer_set, _version) =
let (peer_set, version) =
peerset_protocol_names.try_get_protocol(protocol)?;
gum::trace!(
target: LOG_TARGET,
?peer_set,
?protocol,
?version,
"Received notification"
);
if peer_set == PeerSet::Validation {
if expected_versions[PeerSet::Validation].is_none() {
return Some(Err(UNCONNECTED_PEERSET_COST))
@@ -474,6 +486,16 @@ where
v_messages,
&metrics,
)
} else if expected_versions[PeerSet::Validation] ==
Some(ValidationVersion::VStaging.into())
{
handle_peer_messages::<protocol_vstaging::ValidationProtocol, _>(
remote,
PeerSet::Validation,
&mut shared.0.lock().validation_peers,
v_messages,
&metrics,
)
} else {
gum::warn!(
target: LOG_TARGET,
@@ -815,15 +837,16 @@ fn update_our_view<Net, Context>(
)
};
let filter_by_version = |peers: &[(PeerId, ProtocolVersion)], version| {
peers.iter().filter(|(_, v)| v == &version).map(|(p, _)| *p).collect::<Vec<_>>()
};
let v1_validation_peers =
filter_by_peer_version(&validation_peers, ValidationVersion::V1.into());
let v1_collation_peers = filter_by_peer_version(&collation_peers, CollationVersion::V1.into());
let v1_validation_peers = filter_by_version(&validation_peers, ValidationVersion::V1.into());
let v1_collation_peers = filter_by_version(&collation_peers, CollationVersion::V1.into());
let v2_validation_peers =
filter_by_peer_version(&validation_peers, ValidationVersion::V2.into());
let v2_collation_peers = filter_by_peer_version(&collation_peers, CollationVersion::V2.into());
let v2_validation_peers = filter_by_version(&validation_peers, ValidationVersion::V2.into());
let v2_collation_peers = filter_by_version(&collation_peers, ValidationVersion::V2.into());
let vstaging_validation_peers =
filter_by_peer_version(&validation_peers, ValidationVersion::VStaging.into());
send_validation_message_v1(
net,
@@ -853,7 +876,15 @@ fn update_our_view<Net, Context>(
net,
v2_collation_peers,
peerset_protocol_names,
WireMessage::ViewUpdate(new_view),
WireMessage::ViewUpdate(new_view.clone()),
metrics,
);
send_validation_message_vstaging(
net,
vstaging_validation_peers,
peerset_protocol_names,
WireMessage::ViewUpdate(new_view.clone()),
metrics,
);
@@ -926,78 +957,6 @@ fn handle_peer_messages<RawMessage: Decode, OutMessage: From<RawMessage>>(
(outgoing_events, reports)
}
fn send_validation_message_v1(
net: &mut impl Network,
peers: Vec<PeerId>,
peerset_protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_v1::ValidationProtocol>,
metrics: &Metrics,
) {
send_message(
net,
peers,
PeerSet::Validation,
ValidationVersion::V1.into(),
peerset_protocol_names,
message,
metrics,
);
}
fn send_collation_message_v1(
net: &mut impl Network,
peers: Vec<PeerId>,
peerset_protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_v1::CollationProtocol>,
metrics: &Metrics,
) {
send_message(
net,
peers,
PeerSet::Collation,
CollationVersion::V1.into(),
peerset_protocol_names,
message,
metrics,
);
}
fn send_validation_message_v2(
net: &mut impl Network,
peers: Vec<PeerId>,
protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_v2::ValidationProtocol>,
metrics: &Metrics,
) {
send_message(
net,
peers,
PeerSet::Validation,
ValidationVersion::V2.into(),
protocol_names,
message,
metrics,
);
}
fn send_collation_message_v2(
net: &mut impl Network,
peers: Vec<PeerId>,
protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_v2::CollationProtocol>,
metrics: &Metrics,
) {
send_message(
net,
peers,
PeerSet::Collation,
CollationVersion::V2.into(),
protocol_names,
message,
metrics,
);
}
async fn dispatch_validation_event_to_all(
event: NetworkBridgeEvent<net_protocol::VersionedValidationProtocol>,
ctx: &mut impl overseer::NetworkBridgeRxSenderTrait,
@@ -1248,6 +1248,9 @@ fn network_protocol_versioning_view_update() {
ValidationVersion::V2 =>
WireMessage::<protocol_v2::ValidationProtocol>::ViewUpdate(view.clone())
.encode(),
ValidationVersion::VStaging =>
WireMessage::<protocol_vstaging::ValidationProtocol>::ViewUpdate(view.clone())
.encode(),
};
assert_network_actions_contains(
&actions,
+23 -78
View File
@@ -18,9 +18,7 @@
use super::*;
use polkadot_node_network_protocol::{
peer_set::{CollationVersion, PeerSet, PeerSetProtocolNames, ValidationVersion},
request_response::ReqProtocolNames,
v1 as protocol_v1, v2 as protocol_v2, PeerId, Versioned,
peer_set::PeerSetProtocolNames, request_response::ReqProtocolNames, Versioned,
};
use polkadot_node_subsystem::{
@@ -41,7 +39,10 @@ use crate::validator_discovery;
/// Actual interfacing to the network based on the `Network` trait.
///
/// Defines the `Network` trait with an implementation for an `Arc<NetworkService>`.
use crate::network::{send_message, Network};
use crate::network::{
send_collation_message_v1, send_collation_message_v2, send_validation_message_v1,
send_validation_message_v2, send_validation_message_vstaging, Network,
};
use crate::metrics::Metrics;
@@ -187,6 +188,7 @@ where
gum::trace!(
target: LOG_TARGET,
action = "SendValidationMessages",
?msg,
num_messages = 1usize,
);
@@ -198,6 +200,13 @@ where
WireMessage::ProtocolMessage(msg),
&metrics,
),
Versioned::VStaging(msg) => send_validation_message_vstaging(
&mut network_service,
peers,
peerset_protocol_names,
WireMessage::ProtocolMessage(msg),
&metrics,
),
Versioned::V2(msg) => send_validation_message_v2(
&mut network_service,
peers,
@@ -212,6 +221,7 @@ where
target: LOG_TARGET,
action = "SendValidationMessages",
num_messages = %msgs.len(),
?msgs,
);
for (peers, msg) in msgs {
@@ -223,6 +233,13 @@ where
WireMessage::ProtocolMessage(msg),
&metrics,
),
Versioned::VStaging(msg) => send_validation_message_vstaging(
&mut network_service,
peers,
peerset_protocol_names,
WireMessage::ProtocolMessage(msg),
&metrics,
),
Versioned::V2(msg) => send_validation_message_v2(
&mut network_service,
peers,
@@ -248,7 +265,7 @@ where
WireMessage::ProtocolMessage(msg),
&metrics,
),
Versioned::V2(msg) => send_collation_message_v2(
Versioned::V2(msg) | Versioned::VStaging(msg) => send_collation_message_v2(
&mut network_service,
peers,
peerset_protocol_names,
@@ -273,7 +290,7 @@ where
WireMessage::ProtocolMessage(msg),
&metrics,
),
Versioned::V2(msg) => send_collation_message_v2(
Versioned::V2(msg) | Versioned::VStaging(msg) => send_collation_message_v2(
&mut network_service,
peers,
peerset_protocol_names,
@@ -386,75 +403,3 @@ where
Ok(())
}
fn send_validation_message_v1(
net: &mut impl Network,
peers: Vec<PeerId>,
protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_v1::ValidationProtocol>,
metrics: &Metrics,
) {
send_message(
net,
peers,
PeerSet::Validation,
ValidationVersion::V1.into(),
protocol_names,
message,
metrics,
);
}
fn send_collation_message_v1(
net: &mut impl Network,
peers: Vec<PeerId>,
protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_v1::CollationProtocol>,
metrics: &Metrics,
) {
send_message(
net,
peers,
PeerSet::Collation,
CollationVersion::V1.into(),
protocol_names,
message,
metrics,
);
}
fn send_validation_message_v2(
net: &mut impl Network,
peers: Vec<PeerId>,
protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_v2::ValidationProtocol>,
metrics: &Metrics,
) {
send_message(
net,
peers,
PeerSet::Validation,
ValidationVersion::V2.into(),
protocol_names,
message,
metrics,
);
}
fn send_collation_message_v2(
net: &mut impl Network,
peers: Vec<PeerId>,
protocol_names: &PeerSetProtocolNames,
message: WireMessage<protocol_v2::CollationProtocol>,
metrics: &Metrics,
) {
send_message(
net,
peers,
PeerSet::Collation,
CollationVersion::V2.into(),
protocol_names,
message,
metrics,
);
}
+2 -3
View File
@@ -25,9 +25,9 @@ use std::collections::HashSet;
use sc_network::{Event as NetworkEvent, IfDisconnected, ProtocolName, ReputationChange};
use polkadot_node_network_protocol::{
peer_set::PeerSetProtocolNames,
peer_set::{PeerSetProtocolNames, ValidationVersion},
request_response::{outgoing::Requests, ReqProtocolNames},
ObservedRole, Versioned,
v1 as protocol_v1, v2 as protocol_v2, ObservedRole, Versioned,
};
use polkadot_node_subsystem::{FromOrchestra, OverseerSignal};
use polkadot_node_subsystem_test_helpers::TestSubsystemContextHandle;
@@ -356,7 +356,6 @@ fn network_protocol_versioning_send() {
}
// send a validation protocol message.
{
let approval_distribution_message =
protocol_v2::ApprovalDistributionMessage::Approvals(Vec::new());
@@ -880,7 +880,9 @@ async fn handle_incoming_peer_message<Context>(
use protocol_v2::CollatorProtocolMessage as V2;
match msg {
Versioned::V1(V1::Declare(..)) | Versioned::V2(V2::Declare(..)) => {
Versioned::V1(V1::Declare(..)) |
Versioned::V2(V2::Declare(..)) |
Versioned::VStaging(V2::Declare(..)) => {
gum::trace!(
target: LOG_TARGET,
?origin,
@@ -891,7 +893,9 @@ async fn handle_incoming_peer_message<Context>(
ctx.send_message(NetworkBridgeTxMessage::DisconnectPeer(origin, PeerSet::Collation))
.await;
},
Versioned::V1(V1::AdvertiseCollation(_)) | Versioned::V2(V2::AdvertiseCollation { .. }) => {
Versioned::V1(V1::AdvertiseCollation(_)) |
Versioned::V2(V2::AdvertiseCollation { .. }) |
Versioned::VStaging(V2::AdvertiseCollation { .. }) => {
gum::trace!(
target: LOG_TARGET,
?origin,
@@ -906,7 +910,8 @@ async fn handle_incoming_peer_message<Context>(
.await;
},
Versioned::V1(V1::CollationSeconded(relay_parent, statement)) |
Versioned::V2(V2::CollationSeconded(relay_parent, statement)) => {
Versioned::V2(V2::CollationSeconded(relay_parent, statement)) |
Versioned::VStaging(V2::CollationSeconded(relay_parent, statement)) => {
if !matches!(statement.unchecked_payload(), Statement::Seconded(_)) {
gum::warn!(
target: LOG_TARGET,
@@ -776,7 +776,8 @@ async fn process_incoming_peer_message<Context>(
match msg {
Versioned::V1(V1::Declare(collator_id, para_id, signature)) |
Versioned::V2(V2::Declare(collator_id, para_id, signature)) => {
Versioned::V2(V2::Declare(collator_id, para_id, signature)) |
Versioned::VStaging(V2::Declare(collator_id, para_id, signature)) => {
if collator_peer_id(&state.peer_data, &collator_id).is_some() {
modify_reputation(
&mut state.reputation,
@@ -892,6 +893,11 @@ async fn process_incoming_peer_message<Context>(
relay_parent,
candidate_hash,
parent_head_data_hash,
}) |
Versioned::VStaging(V2::AdvertiseCollation {
relay_parent,
candidate_hash,
parent_head_data_hash,
}) =>
if let Err(err) = handle_advertisement(
ctx.sender(),
@@ -915,7 +921,9 @@ async fn process_incoming_peer_message<Context>(
modify_reputation(&mut state.reputation, ctx.sender(), origin, rep).await;
}
},
Versioned::V1(V1::CollationSeconded(..)) | Versioned::V2(V2::CollationSeconded(..)) => {
Versioned::V1(V1::CollationSeconded(..)) |
Versioned::V2(V2::CollationSeconded(..)) |
Versioned::VStaging(V2::CollationSeconded(..)) => {
gum::warn!(
target: LOG_TARGET,
peer_id = ?origin,
@@ -477,6 +477,7 @@ where
match message {
Versioned::V1(m) => match m {},
Versioned::V2(m) => match m {},
Versioned::VStaging(m) => match m {},
}
},
}
@@ -27,3 +27,6 @@ bitvec = "1"
[dev-dependencies]
rand_chacha = "0.3.1"
[features]
network-protocol-staging = []
+111 -19
View File
@@ -253,25 +253,29 @@ impl View {
/// A protocol-versioned type.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Versioned<V1, V2> {
pub enum Versioned<V1, V2, VStaging = V2> {
/// V1 type.
V1(V1),
/// V2 type.
V2(V2),
/// VStaging type
VStaging(VStaging),
}
impl<V1: Clone, V2: Clone> Versioned<&'_ V1, &'_ V2> {
impl<V1: Clone, V2: Clone, VStaging: Clone> Versioned<&'_ V1, &'_ V2, &'_ VStaging> {
/// Convert to a fully-owned version of the message.
pub fn clone_inner(&self) -> Versioned<V1, V2> {
pub fn clone_inner(&self) -> Versioned<V1, V2, VStaging> {
match *self {
Versioned::V1(inner) => Versioned::V1(inner.clone()),
Versioned::V2(inner) => Versioned::V2(inner.clone()),
Versioned::VStaging(inner) => Versioned::VStaging(inner.clone()),
}
}
}
/// All supported versions of the validation protocol message.
pub type VersionedValidationProtocol = Versioned<v1::ValidationProtocol, v2::ValidationProtocol>;
pub type VersionedValidationProtocol =
Versioned<v1::ValidationProtocol, v2::ValidationProtocol, vstaging::ValidationProtocol>;
impl From<v1::ValidationProtocol> for VersionedValidationProtocol {
fn from(v1: v1::ValidationProtocol) -> Self {
@@ -285,6 +289,12 @@ impl From<v2::ValidationProtocol> for VersionedValidationProtocol {
}
}
impl From<vstaging::ValidationProtocol> for VersionedValidationProtocol {
fn from(vstaging: vstaging::ValidationProtocol) -> Self {
VersionedValidationProtocol::VStaging(vstaging)
}
}
/// All supported versions of the collation protocol message.
pub type VersionedCollationProtocol = Versioned<v1::CollationProtocol, v2::CollationProtocol>;
@@ -307,12 +317,12 @@ macro_rules! impl_versioned_full_protocol_from {
match versioned_from {
Versioned::V1(x) => Versioned::V1(x.into()),
Versioned::V2(x) => Versioned::V2(x.into()),
Versioned::VStaging(x) => Versioned::VStaging(x.into()),
}
}
}
};
}
/// Implement `TryFrom` for one versioned enum variant into the inner type.
/// `$m_ty::$variant(inner) -> Ok(inner)`
macro_rules! impl_versioned_try_from {
@@ -320,7 +330,8 @@ macro_rules! impl_versioned_try_from {
$from:ty,
$out:ty,
$v1_pat:pat => $v1_out:expr,
$v2_pat:pat => $v2_out:expr
$v2_pat:pat => $v2_out:expr,
$vstaging_pat:pat => $vstaging_out:expr
) => {
impl TryFrom<$from> for $out {
type Error = crate::WrongVariant;
@@ -330,6 +341,7 @@ macro_rules! impl_versioned_try_from {
match x {
Versioned::V1($v1_pat) => Ok(Versioned::V1($v1_out)),
Versioned::V2($v2_pat) => Ok(Versioned::V2($v2_out)),
Versioned::VStaging($vstaging_pat) => Ok(Versioned::VStaging($vstaging_out)),
_ => Err(crate::WrongVariant),
}
}
@@ -343,6 +355,8 @@ macro_rules! impl_versioned_try_from {
match x {
Versioned::V1($v1_pat) => Ok(Versioned::V1($v1_out.clone())),
Versioned::V2($v2_pat) => Ok(Versioned::V2($v2_out.clone())),
Versioned::VStaging($vstaging_pat) =>
Ok(Versioned::VStaging($vstaging_out.clone())),
_ => Err(crate::WrongVariant),
}
}
@@ -351,8 +365,11 @@ macro_rules! impl_versioned_try_from {
}
/// Version-annotated messages used by the bitfield distribution subsystem.
pub type BitfieldDistributionMessage =
Versioned<v1::BitfieldDistributionMessage, v2::BitfieldDistributionMessage>;
pub type BitfieldDistributionMessage = Versioned<
v1::BitfieldDistributionMessage,
v2::BitfieldDistributionMessage,
vstaging::BitfieldDistributionMessage,
>;
impl_versioned_full_protocol_from!(
BitfieldDistributionMessage,
VersionedValidationProtocol,
@@ -362,12 +379,16 @@ impl_versioned_try_from!(
VersionedValidationProtocol,
BitfieldDistributionMessage,
v1::ValidationProtocol::BitfieldDistribution(x) => x,
v2::ValidationProtocol::BitfieldDistribution(x) => x
v2::ValidationProtocol::BitfieldDistribution(x) => x,
vstaging::ValidationProtocol::BitfieldDistribution(x) => x
);
/// Version-annotated messages used by the statement distribution subsystem.
pub type StatementDistributionMessage =
Versioned<v1::StatementDistributionMessage, v2::StatementDistributionMessage>;
pub type StatementDistributionMessage = Versioned<
v1::StatementDistributionMessage,
v2::StatementDistributionMessage,
vstaging::StatementDistributionMessage,
>;
impl_versioned_full_protocol_from!(
StatementDistributionMessage,
VersionedValidationProtocol,
@@ -377,12 +398,16 @@ impl_versioned_try_from!(
VersionedValidationProtocol,
StatementDistributionMessage,
v1::ValidationProtocol::StatementDistribution(x) => x,
v2::ValidationProtocol::StatementDistribution(x) => x
v2::ValidationProtocol::StatementDistribution(x) => x,
vstaging::ValidationProtocol::StatementDistribution(x) => x
);
/// Version-annotated messages used by the approval distribution subsystem.
pub type ApprovalDistributionMessage =
Versioned<v1::ApprovalDistributionMessage, v2::ApprovalDistributionMessage>;
pub type ApprovalDistributionMessage = Versioned<
v1::ApprovalDistributionMessage,
v2::ApprovalDistributionMessage,
vstaging::ApprovalDistributionMessage,
>;
impl_versioned_full_protocol_from!(
ApprovalDistributionMessage,
VersionedValidationProtocol,
@@ -392,13 +417,18 @@ impl_versioned_try_from!(
VersionedValidationProtocol,
ApprovalDistributionMessage,
v1::ValidationProtocol::ApprovalDistribution(x) => x,
v2::ValidationProtocol::ApprovalDistribution(x) => x
v2::ValidationProtocol::ApprovalDistribution(x) => x,
vstaging::ValidationProtocol::ApprovalDistribution(x) => x
);
/// Version-annotated messages used by the gossip-support subsystem (this is void).
pub type GossipSupportNetworkMessage =
Versioned<v1::GossipSupportNetworkMessage, v2::GossipSupportNetworkMessage>;
pub type GossipSupportNetworkMessage = Versioned<
v1::GossipSupportNetworkMessage,
v2::GossipSupportNetworkMessage,
vstaging::GossipSupportNetworkMessage,
>;
// This is a void enum placeholder, so never gets sent over the wire.
impl TryFrom<VersionedValidationProtocol> for GossipSupportNetworkMessage {
type Error = WrongVariant;
@@ -426,6 +456,7 @@ impl_versioned_try_from!(
VersionedCollationProtocol,
CollatorProtocolMessage,
v1::CollationProtocol::CollatorProtocol(x) => x,
v2::CollationProtocol::CollatorProtocol(x) => x,
v2::CollationProtocol::CollatorProtocol(x) => x
);
@@ -439,7 +470,7 @@ pub mod v1 {
};
use polkadot_node_primitives::{
approval::{IndirectAssignmentCert, IndirectSignedApprovalVote},
approval::v1::{IndirectAssignmentCert, IndirectSignedApprovalVote},
UncheckedSignedFullStatement,
};
@@ -598,7 +629,7 @@ pub mod v2 {
};
use polkadot_node_primitives::{
approval::{IndirectAssignmentCert, IndirectSignedApprovalVote},
approval::v1::{IndirectAssignmentCert, IndirectSignedApprovalVote},
UncheckedSignedFullStatement,
};
@@ -839,3 +870,64 @@ pub mod v2 {
payload
}
}
/// vstaging network protocol types, intended to become v3.
/// Initial purpose is for chaning ApprovalDistributionMessage to
/// include more than one assignment in the message.
pub mod vstaging {
use parity_scale_codec::{Decode, Encode};
use polkadot_node_primitives::approval::{
v1::IndirectSignedApprovalVote,
v2::{CandidateBitfield, IndirectAssignmentCertV2},
};
/// This parts of the protocol did not change from v2, so just alias them in vstaging,
/// no reason why they can't be change untill vstaging becomes v3 and is released.
pub use super::v2::{
declare_signature_payload, BackedCandidateAcknowledgement, BackedCandidateManifest,
BitfieldDistributionMessage, GossipSupportNetworkMessage, StatementDistributionMessage,
StatementFilter,
};
/// Network messages used by the approval distribution subsystem.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum ApprovalDistributionMessage {
/// Assignments for candidates in recent, unfinalized blocks.
/// We use a bitfield to reference claimed candidates, where the bit index is equal to
/// candidate index.
///
/// Actually checking the assignment may yield a different result.
/// TODO: Look at getting rid of bitfield in the future.
#[codec(index = 0)]
Assignments(Vec<(IndirectAssignmentCertV2, CandidateBitfield)>),
/// Approvals for candidates in some recent, unfinalized block.
#[codec(index = 1)]
Approvals(Vec<IndirectSignedApprovalVote>),
}
/// All network messages on the validation peer-set.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq, derive_more::From)]
pub enum ValidationProtocol {
/// Bitfield distribution messages
#[codec(index = 1)]
#[from]
BitfieldDistribution(BitfieldDistributionMessage),
/// Statement distribution messages
#[codec(index = 3)]
#[from]
StatementDistribution(StatementDistributionMessage),
/// Approval distribution messages
#[codec(index = 4)]
#[from]
ApprovalDistribution(ApprovalDistributionMessage),
}
}
/// Returns the subset of `peers` with the specified `version`.
pub fn filter_by_peer_version(
peers: &[(PeerId, peer_set::ProtocolVersion)],
version: peer_set::ProtocolVersion,
) -> Vec<PeerId> {
peers.iter().filter(|(_, v)| v == &version).map(|(p, _)| *p).collect::<Vec<_>>()
}
@@ -118,10 +118,17 @@ impl PeerSet {
/// Networking layer relies on `get_main_version()` being the version
/// of the main protocol name reported by [`PeerSetProtocolNames::get_main_name()`].
pub fn get_main_version(self) -> ProtocolVersion {
#[cfg(not(feature = "network-protocol-staging"))]
match self {
PeerSet::Validation => ValidationVersion::V2.into(),
PeerSet::Collation => CollationVersion::V2.into(),
}
#[cfg(feature = "network-protocol-staging")]
match self {
PeerSet::Validation => ValidationVersion::VStaging.into(),
PeerSet::Collation => CollationVersion::V2.into(),
}
}
/// Get the max notification size for this peer set.
@@ -147,6 +154,8 @@ impl PeerSet {
Some("validation/1")
} else if version == ValidationVersion::V2.into() {
Some("validation/2")
} else if version == ValidationVersion::VStaging.into() {
Some("validation/3")
} else {
None
},
@@ -218,6 +227,9 @@ pub enum ValidationVersion {
V1 = 1,
/// The second version.
V2 = 2,
/// The staging version to gather changes
/// that before the release become v3.
VStaging = 3,
}
/// Supported collation protocol versions. Only versions defined here must be used in the codebase.
@@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
use net_protocol::{filter_by_peer_version, peer_set::ProtocolVersion};
use parity_scale_codec::Encode;
use polkadot_node_network_protocol::{
@@ -21,7 +22,8 @@ use polkadot_node_network_protocol::{
grid_topology::{GridNeighbors, RequiredRouting, SessionBoundGridTopologyStorage},
peer_set::{IsAuthority, PeerSet, ValidationVersion},
v1::{self as protocol_v1, StatementMetadata},
v2 as protocol_v2, IfDisconnected, PeerId, UnifiedReputationChange as Rep, Versioned, View,
v2 as protocol_v2, vstaging as protocol_vstaging, IfDisconnected, PeerId,
UnifiedReputationChange as Rep, Versioned, View,
};
use polkadot_node_primitives::{
SignedFullStatement, Statement, StatementWithPVD, UncheckedSignedFullStatement,
@@ -1061,7 +1063,7 @@ async fn circulate_statement<'a, Context>(
"We filter out duplicates above. qed.",
);
let (v1_peers_to_send, v2_peers_to_send) = peers_to_send
let (v1_peers_to_send, non_v1_peers_to_send) = peers_to_send
.into_iter()
.map(|peer_id| {
let peer_data =
@@ -1073,7 +1075,7 @@ async fn circulate_statement<'a, Context>(
})
.partition::<Vec<_>, _>(|(_, _, version)| match version {
ValidationVersion::V1 => true,
ValidationVersion::V2 => false,
ValidationVersion::V2 | ValidationVersion::VStaging => false,
}); // partition is handy here but not if we add more protocol versions
let payload = v1_statement_message(relay_parent, stored.statement.clone(), metrics);
@@ -1093,6 +1095,22 @@ async fn circulate_statement<'a, Context>(
))
.await;
}
let peers_to_send: Vec<(PeerId, ProtocolVersion)> = non_v1_peers_to_send
.iter()
.map(|(p, _, version)| (*p, (*version).into()))
.collect();
let peer_needs_dependent_statement = v1_peers_to_send
.into_iter()
.chain(non_v1_peers_to_send)
.filter_map(|(peer, needs_dependent, _)| if needs_dependent { Some(peer) } else { None })
.collect();
let v2_peers_to_send = filter_by_peer_version(&peers_to_send, ValidationVersion::V2.into());
let vstaging_to_send =
filter_by_peer_version(&peers_to_send, ValidationVersion::VStaging.into());
if !v2_peers_to_send.is_empty() {
gum::trace!(
target: LOG_TARGET,
@@ -1102,17 +1120,28 @@ async fn circulate_statement<'a, Context>(
"Sending statement to v2 peers",
);
ctx.send_message(NetworkBridgeTxMessage::SendValidationMessage(
v2_peers_to_send.iter().map(|(p, _, _)| *p).collect(),
v2_peers_to_send,
compatible_v1_message(ValidationVersion::V2, payload.clone()).into(),
))
.await;
}
v1_peers_to_send
.into_iter()
.chain(v2_peers_to_send)
.filter_map(|(peer, needs_dependent, _)| if needs_dependent { Some(peer) } else { None })
.collect()
if !vstaging_to_send.is_empty() {
gum::trace!(
target: LOG_TARGET,
?vstaging_to_send,
?relay_parent,
statement = ?stored.statement,
"Sending statement to vstaging peers",
);
ctx.send_message(NetworkBridgeTxMessage::SendValidationMessage(
vstaging_to_send,
compatible_v1_message(ValidationVersion::VStaging, payload.clone()).into(),
))
.await;
}
peer_needs_dependent_statement
}
/// Send all statements about a given candidate hash to a peer.
@@ -1442,8 +1471,11 @@ async fn handle_incoming_message<'a, Context>(
let message = match message {
Versioned::V1(m) => m,
Versioned::V2(protocol_v2::StatementDistributionMessage::V1Compatibility(m)) => m,
Versioned::V2(_) => {
Versioned::V2(protocol_v2::StatementDistributionMessage::V1Compatibility(m)) |
Versioned::VStaging(protocol_vstaging::StatementDistributionMessage::V1Compatibility(
m,
)) => m,
Versioned::V2(_) | Versioned::VStaging(_) => {
// The higher-level subsystem code is supposed to filter out
// all non v1 messages.
gum::debug!(
@@ -2169,5 +2201,8 @@ fn compatible_v1_message(
ValidationVersion::V1 => Versioned::V1(message),
ValidationVersion::V2 =>
Versioned::V2(protocol_v2::StatementDistributionMessage::V1Compatibility(message)),
ValidationVersion::VStaging => Versioned::VStaging(
protocol_vstaging::StatementDistributionMessage::V1Compatibility(message),
),
}
}
@@ -27,7 +27,7 @@ use std::time::Duration;
use polkadot_node_network_protocol::{
request_response::{v1 as request_v1, v2::AttestedCandidateRequest, IncomingRequestReceiver},
v2 as protocol_v2, Versioned,
v2 as protocol_v2, vstaging as protocol_vstaging, Versioned,
};
use polkadot_node_primitives::StatementWithPVD;
use polkadot_node_subsystem::{
@@ -399,9 +399,12 @@ impl<R: rand::Rng> StatementDistributionSubsystem<R> {
NetworkBridgeEvent::PeerMessage(_, message) => match message {
Versioned::V2(
protocol_v2::StatementDistributionMessage::V1Compatibility(_),
) |
Versioned::VStaging(
protocol_vstaging::StatementDistributionMessage::V1Compatibility(_),
) => VersionTarget::Legacy,
Versioned::V1(_) => VersionTarget::Legacy,
Versioned::V2(_) => VersionTarget::Current,
Versioned::V2(_) | Versioned::VStaging(_) => VersionTarget::Current,
},
_ => VersionTarget::Both,
};
@@ -17,6 +17,7 @@
//! Implementation of the v2 statement distribution protocol,
//! designed for asynchronous backing.
use net_protocol::{filter_by_peer_version, peer_set::ProtocolVersion};
use polkadot_node_network_protocol::{
self as net_protocol,
grid_topology::SessionGridTopology,
@@ -28,7 +29,8 @@ use polkadot_node_network_protocol::{
MAX_PARALLEL_ATTESTED_CANDIDATE_REQUESTS,
},
v2::{self as protocol_v2, StatementFilter},
IfDisconnected, PeerId, UnifiedReputationChange as Rep, Versioned, View,
vstaging as protocol_vstaging, IfDisconnected, PeerId, UnifiedReputationChange as Rep,
Versioned, View,
};
use polkadot_node_primitives::{
SignedFullStatementWithPVD, StatementWithPVD as FullStatementWithPVD,
@@ -260,6 +262,7 @@ fn connected_validator_peer(
struct PeerState {
view: View,
protocol_version: ValidationVersion,
implicit_view: HashSet<Hash>,
discovery_ids: Option<HashSet<AuthorityDiscoveryId>>,
}
@@ -332,9 +335,13 @@ pub(crate) async fn handle_network_update<Context>(
NetworkBridgeEvent::PeerConnected(peer_id, role, protocol_version, mut authority_ids) => {
gum::trace!(target: LOG_TARGET, ?peer_id, ?role, ?protocol_version, "Peer connected");
if protocol_version != ValidationVersion::V2.into() {
let versioned_protocol = if protocol_version != ValidationVersion::V2.into() &&
protocol_version != ValidationVersion::VStaging.into()
{
return
}
} else {
protocol_version.try_into().expect("Qed, we checked above")
};
if let Some(ref mut authority_ids) = authority_ids {
authority_ids.retain(|a| match state.authorities.entry(a.clone()) {
@@ -361,6 +368,7 @@ pub(crate) async fn handle_network_update<Context>(
PeerState {
view: View::default(),
implicit_view: HashSet::new(),
protocol_version: versioned_protocol,
discovery_ids: authority_ids,
},
);
@@ -393,17 +401,29 @@ pub(crate) async fn handle_network_update<Context>(
net_protocol::StatementDistributionMessage::V1(_) => return,
net_protocol::StatementDistributionMessage::V2(
protocol_v2::StatementDistributionMessage::V1Compatibility(_),
) |
net_protocol::StatementDistributionMessage::VStaging(
protocol_vstaging::StatementDistributionMessage::V1Compatibility(_),
) => return,
net_protocol::StatementDistributionMessage::V2(
protocol_v2::StatementDistributionMessage::Statement(relay_parent, statement),
) |
net_protocol::StatementDistributionMessage::VStaging(
protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, statement),
) =>
handle_incoming_statement(ctx, state, peer_id, relay_parent, statement, reputation)
.await,
net_protocol::StatementDistributionMessage::V2(
protocol_v2::StatementDistributionMessage::BackedCandidateManifest(inner),
) |
net_protocol::StatementDistributionMessage::VStaging(
protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest(inner),
) => handle_incoming_manifest(ctx, state, peer_id, inner, reputation).await,
net_protocol::StatementDistributionMessage::V2(
protocol_v2::StatementDistributionMessage::BackedCandidateKnown(inner),
) |
net_protocol::StatementDistributionMessage::VStaging(
protocol_vstaging::StatementDistributionMessage::BackedCandidateKnown(inner),
) => handle_incoming_acknowledgement(ctx, state, peer_id, inner, reputation).await,
},
NetworkBridgeEvent::PeerViewChange(peer_id, view) =>
@@ -709,7 +729,7 @@ async fn send_peer_messages_for_relay_parent<Context>(
send_pending_cluster_statements(
ctx,
relay_parent,
&peer,
&(peer, peer_data.protocol_version),
validator_id,
&mut local_validator_state.cluster_tracker,
&state.candidates,
@@ -721,7 +741,7 @@ async fn send_peer_messages_for_relay_parent<Context>(
send_pending_grid_messages(
ctx,
relay_parent,
&peer,
&(peer, peer_data.protocol_version),
validator_id,
&per_session_state.groups,
relay_parent_state,
@@ -734,15 +754,34 @@ async fn send_peer_messages_for_relay_parent<Context>(
fn pending_statement_network_message(
statement_store: &StatementStore,
relay_parent: Hash,
peer: &PeerId,
peer: &(PeerId, ValidationVersion),
originator: ValidatorIndex,
compact: CompactStatement,
) -> Option<(Vec<PeerId>, net_protocol::VersionedValidationProtocol)> {
statement_store
.validator_statement(originator, compact)
.map(|s| s.as_unchecked().clone())
.map(|signed| protocol_v2::StatementDistributionMessage::Statement(relay_parent, signed))
.map(|msg| (vec![*peer], Versioned::V2(msg).into()))
match peer.1 {
ValidationVersion::V2 => statement_store
.validator_statement(originator, compact)
.map(|s| s.as_unchecked().clone())
.map(|signed| {
protocol_v2::StatementDistributionMessage::Statement(relay_parent, signed)
})
.map(|msg| (vec![peer.0], Versioned::V2(msg).into())),
ValidationVersion::VStaging => statement_store
.validator_statement(originator, compact)
.map(|s| s.as_unchecked().clone())
.map(|signed| {
protocol_vstaging::StatementDistributionMessage::Statement(relay_parent, signed)
})
.map(|msg| (vec![peer.0], Versioned::VStaging(msg).into())),
ValidationVersion::V1 => {
gum::error!(
target: LOG_TARGET,
"Bug ValidationVersion::V1 should not be used in statement-distribution v2,
legacy should have handled this"
);
None
},
}
}
/// Send a peer all pending cluster statements for a relay parent.
@@ -750,7 +789,7 @@ fn pending_statement_network_message(
async fn send_pending_cluster_statements<Context>(
ctx: &mut Context,
relay_parent: Hash,
peer_id: &PeerId,
peer_id: &(PeerId, ValidationVersion),
peer_validator_id: ValidatorIndex,
cluster_tracker: &mut ClusterTracker,
candidates: &Candidates,
@@ -794,7 +833,7 @@ async fn send_pending_cluster_statements<Context>(
async fn send_pending_grid_messages<Context>(
ctx: &mut Context,
relay_parent: Hash,
peer_id: &PeerId,
peer_id: &(PeerId, ValidationVersion),
peer_validator_id: ValidatorIndex,
groups: &Groups,
relay_parent_state: &mut PerRelayParentState,
@@ -856,20 +895,37 @@ async fn send_pending_grid_messages<Context>(
candidate_hash,
local_knowledge.clone(),
);
messages.push((
vec![*peer_id],
Versioned::V2(
protocol_v2::StatementDistributionMessage::BackedCandidateManifest(
manifest,
),
)
.into(),
));
match peer_id.1 {
ValidationVersion::V2 => messages.push((
vec![peer_id.0],
Versioned::V2(
protocol_v2::StatementDistributionMessage::BackedCandidateManifest(
manifest,
),
)
.into(),
)),
ValidationVersion::VStaging => messages.push((
vec![peer_id.0],
Versioned::VStaging(
protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest(
manifest,
),
)
.into(),
)),
ValidationVersion::V1 => {
gum::error!(
target: LOG_TARGET,
"Bug ValidationVersion::V1 should not be used in statement-distribution v2,
legacy should have handled this"
);
}
};
},
grid::ManifestKind::Acknowledgement => {
messages.extend(acknowledgement_and_statement_messages(
*peer_id,
peer_id,
peer_validator_id,
groups,
relay_parent_state,
@@ -1156,11 +1212,18 @@ async fn circulate_statement<Context>(
(local_validator, targets)
};
let mut statement_to = Vec::new();
let mut statement_to_peers: Vec<(PeerId, ProtocolVersion)> = Vec::new();
for (target, authority_id, kind) in targets {
// Find peer ID based on authority ID, and also filter to connected.
let peer_id: PeerId = match authorities.get(&authority_id) {
Some(p) if peers.get(p).map_or(false, |p| p.knows_relay_parent(&relay_parent)) => *p,
let peer_id: (PeerId, ProtocolVersion) = match authorities.get(&authority_id) {
Some(p) if peers.get(p).map_or(false, |p| p.knows_relay_parent(&relay_parent)) => (
*p,
peers
.get(p)
.expect("Qed, can't fail because it was checked above")
.protocol_version
.into(),
),
None | Some(_) => continue,
};
@@ -1178,11 +1241,11 @@ async fn circulate_statement<Context>(
originator,
compact_statement.clone(),
);
statement_to.push(peer_id);
statement_to_peers.push(peer_id);
}
},
DirectTargetKind::Grid => {
statement_to.push(peer_id);
statement_to_peers.push(peer_id);
local_validator.grid_tracker.sent_or_received_direct_statement(
&per_session.groups,
originator,
@@ -1193,17 +1256,23 @@ async fn circulate_statement<Context>(
}
}
let statement_to_v2_peers =
filter_by_peer_version(&statement_to_peers, ValidationVersion::V2.into());
let statement_to_vstaging_peers =
filter_by_peer_version(&statement_to_peers, ValidationVersion::VStaging.into());
// ship off the network messages to the network bridge.
if !statement_to.is_empty() {
if !statement_to_v2_peers.is_empty() {
gum::debug!(
target: LOG_TARGET,
?compact_statement,
n_peers = ?statement_to.len(),
"Sending statement to peers",
n_peers = ?statement_to_v2_peers.len(),
"Sending statement to v2 peers",
);
ctx.send_message(NetworkBridgeTxMessage::SendValidationMessage(
statement_to,
statement_to_v2_peers,
Versioned::V2(protocol_v2::StatementDistributionMessage::Statement(
relay_parent,
statement.as_unchecked().clone(),
@@ -1212,6 +1281,25 @@ async fn circulate_statement<Context>(
))
.await;
}
if !statement_to_vstaging_peers.is_empty() {
gum::debug!(
target: LOG_TARGET,
?compact_statement,
n_peers = ?statement_to_peers.len(),
"Sending statement to vstaging peers",
);
ctx.send_message(NetworkBridgeTxMessage::SendValidationMessage(
statement_to_vstaging_peers,
Versioned::VStaging(protocol_vstaging::StatementDistributionMessage::Statement(
relay_parent,
statement.as_unchecked().clone(),
))
.into(),
))
.await;
}
}
/// Check a statement signature under this parent hash.
fn check_statement_signature(
@@ -1697,14 +1785,8 @@ async fn provide_candidate_to_grid<Context>(
statement_knowledge: filter.clone(),
};
let manifest_message =
Versioned::V2(protocol_v2::StatementDistributionMessage::BackedCandidateManifest(manifest));
let ack_message = Versioned::V2(
protocol_v2::StatementDistributionMessage::BackedCandidateKnown(acknowledgement),
);
let mut manifest_peers = Vec::new();
let mut ack_peers = Vec::new();
let mut manifest_peers: Vec<(PeerId, ProtocolVersion)> = Vec::new();
let mut ack_peers: Vec<(PeerId, ProtocolVersion)> = Vec::new();
let mut post_statements = Vec::new();
for (v, action) in actions {
@@ -1712,7 +1794,7 @@ async fn provide_candidate_to_grid<Context>(
None => continue,
Some(p) =>
if peers.get(&p).map_or(false, |d| d.knows_relay_parent(&relay_parent)) {
p
(p, peers.get(&p).expect("Qed, was checked above").protocol_version.into())
} else {
continue
},
@@ -1738,44 +1820,95 @@ async fn provide_candidate_to_grid<Context>(
&per_session.groups,
group_index,
candidate_hash,
&(p.0, p.1.try_into().expect("Qed, can not fail was checked above")),
)
.into_iter()
.map(|m| (vec![p], m)),
.map(|m| (vec![p.0], m)),
);
}
if !manifest_peers.is_empty() {
let manifest_peers_v2 = filter_by_peer_version(&manifest_peers, ValidationVersion::V2.into());
let manifest_peers_vstaging =
filter_by_peer_version(&manifest_peers, ValidationVersion::VStaging.into());
if !manifest_peers_v2.is_empty() {
gum::debug!(
target: LOG_TARGET,
?candidate_hash,
local_validator = ?local_validator.index,
n_peers = manifest_peers.len(),
"Sending manifest to peers"
n_peers = manifest_peers_v2.len(),
"Sending manifest to v2 peers"
);
ctx.send_message(NetworkBridgeTxMessage::SendValidationMessage(
manifest_peers,
manifest_message.into(),
manifest_peers_v2,
Versioned::V2(protocol_v2::StatementDistributionMessage::BackedCandidateManifest(
manifest.clone(),
))
.into(),
))
.await;
}
if !ack_peers.is_empty() {
if !manifest_peers_vstaging.is_empty() {
gum::debug!(
target: LOG_TARGET,
?candidate_hash,
local_validator = ?local_validator.index,
n_peers = ack_peers.len(),
"Sending acknowledgement to peers"
n_peers = manifest_peers_vstaging.len(),
"Sending manifest to vstaging peers"
);
ctx.send_message(NetworkBridgeTxMessage::SendValidationMessage(
ack_peers,
ack_message.into(),
manifest_peers_vstaging,
Versioned::VStaging(
protocol_vstaging::StatementDistributionMessage::BackedCandidateManifest(manifest),
)
.into(),
))
.await;
}
let ack_peers_v2 = filter_by_peer_version(&ack_peers, ValidationVersion::V2.into());
let ack_peers_vstaging = filter_by_peer_version(&ack_peers, ValidationVersion::VStaging.into());
if !ack_peers_v2.is_empty() {
gum::debug!(
target: LOG_TARGET,
?candidate_hash,
local_validator = ?local_validator.index,
n_peers = ack_peers_v2.len(),
"Sending acknowledgement to v2 peers"
);
ctx.send_message(NetworkBridgeTxMessage::SendValidationMessage(
ack_peers_v2,
Versioned::V2(protocol_v2::StatementDistributionMessage::BackedCandidateKnown(
acknowledgement.clone(),
))
.into(),
))
.await;
}
if !ack_peers_vstaging.is_empty() {
gum::debug!(
target: LOG_TARGET,
?candidate_hash,
local_validator = ?local_validator.index,
n_peers = ack_peers_vstaging.len(),
"Sending acknowledgement to vstaging peers"
);
ctx.send_message(NetworkBridgeTxMessage::SendValidationMessage(
ack_peers_vstaging,
Versioned::VStaging(
protocol_vstaging::StatementDistributionMessage::BackedCandidateKnown(
acknowledgement,
),
)
.into(),
))
.await;
}
if !post_statements.is_empty() {
ctx.send_message(NetworkBridgeTxMessage::SendValidationMessages(post_statements))
.await;
@@ -2074,6 +2207,7 @@ fn post_acknowledgement_statement_messages(
groups: &Groups,
group_index: GroupIndex,
candidate_hash: CandidateHash,
peer: &(PeerId, ValidationVersion),
) -> Vec<net_protocol::VersionedValidationProtocol> {
let sending_filter = match grid_tracker.pending_statements_for(recipient, candidate_hash) {
None => return Vec::new(),
@@ -2090,14 +2224,29 @@ fn post_acknowledgement_statement_messages(
recipient,
statement.payload(),
);
messages.push(Versioned::V2(
protocol_v2::StatementDistributionMessage::Statement(
relay_parent,
statement.as_unchecked().clone(),
)
.into(),
));
match peer.1.into() {
ValidationVersion::V2 => messages.push(Versioned::V2(
protocol_v2::StatementDistributionMessage::Statement(
relay_parent,
statement.as_unchecked().clone(),
)
.into(),
)),
ValidationVersion::VStaging => messages.push(Versioned::VStaging(
protocol_vstaging::StatementDistributionMessage::Statement(
relay_parent,
statement.as_unchecked().clone(),
)
.into(),
)),
ValidationVersion::V1 => {
gum::error!(
target: LOG_TARGET,
"Bug ValidationVersion::V1 should not be used in statement-distribution v2,
legacy should have handled this"
);
},
};
}
messages
@@ -2167,7 +2316,15 @@ async fn handle_incoming_manifest<Context>(
};
let messages = acknowledgement_and_statement_messages(
peer,
&(
peer,
state
.peers
.get(&peer)
.map(|val| val.protocol_version)
// Assume the latest stable version, if we don't have info about peer version.
.unwrap_or(ValidationVersion::V2),
),
sender_index,
&per_session.groups,
relay_parent_state,
@@ -2198,7 +2355,7 @@ async fn handle_incoming_manifest<Context>(
/// Produces acknowledgement and statement messages to be sent over the network,
/// noting that they have been sent within the grid topology tracker as well.
fn acknowledgement_and_statement_messages(
peer: PeerId,
peer: &(PeerId, ValidationVersion),
validator_index: ValidatorIndex,
groups: &Groups,
relay_parent_state: &mut PerRelayParentState,
@@ -2217,11 +2374,28 @@ fn acknowledgement_and_statement_messages(
statement_knowledge: local_knowledge.clone(),
};
let msg = Versioned::V2(protocol_v2::StatementDistributionMessage::BackedCandidateKnown(
acknowledgement,
let msg_v2 = Versioned::V2(protocol_v2::StatementDistributionMessage::BackedCandidateKnown(
acknowledgement.clone(),
));
let mut messages = vec![(vec![peer], msg.into())];
let mut messages = match peer.1 {
ValidationVersion::V2 => vec![(vec![peer.0], msg_v2.into())],
ValidationVersion::VStaging => vec![(
vec![peer.0],
Versioned::VStaging(protocol_v2::StatementDistributionMessage::BackedCandidateKnown(
acknowledgement,
))
.into(),
)],
ValidationVersion::V1 => {
gum::error!(
target: LOG_TARGET,
"Bug ValidationVersion::V1 should not be used in statement-distribution v2,
legacy should have handled this"
);
return Vec::new()
},
};
local_validator.grid_tracker.manifest_sent_to(
groups,
@@ -2238,9 +2412,10 @@ fn acknowledgement_and_statement_messages(
&groups,
group_index,
candidate_hash,
peer,
);
messages.extend(statement_messages.into_iter().map(|m| (vec![peer], m)));
messages.extend(statement_messages.into_iter().map(|m| (vec![peer.0], m)));
messages
}
@@ -2320,6 +2495,15 @@ async fn handle_incoming_acknowledgement<Context>(
&per_session.groups,
group_index,
candidate_hash,
&(
peer,
state
.peers
.get(&peer)
.map(|val| val.protocol_version)
// Assume the latest stable version, if we don't have info about peer version.
.unwrap_or(ValidationVersion::V2),
),
);
if !messages.is_empty() {
+1
View File
@@ -20,6 +20,7 @@ sp-runtime = { path = "../../../substrate/primitives/runtime" }
polkadot-parachain-primitives = { path = "../../parachain", default-features = false }
schnorrkel = "0.9.1"
thiserror = "1.0.48"
bitvec = { version = "1.0.0", default-features = false, features = ["alloc"] }
serde = { version = "1.0.188", features = ["derive"] }
[target.'cfg(not(target_os = "unknown"))'.dependencies]
+487 -162
View File
@@ -16,190 +16,515 @@
//! Types relevant for approval.
pub use sp_consensus_babe::{Randomness, Slot, VrfOutput, VrfProof, VrfSignature, VrfTranscript};
/// A list of primitives introduced in v1.
pub mod v1 {
use sp_consensus_babe as babe_primitives;
pub use sp_consensus_babe::{
Randomness, Slot, VrfOutput, VrfProof, VrfSignature, VrfTranscript,
};
use parity_scale_codec::{Decode, Encode};
use polkadot_primitives::{
BlockNumber, CandidateHash, CandidateIndex, CoreIndex, Hash, Header, SessionIndex,
ValidatorIndex, ValidatorSignature,
};
use sp_application_crypto::ByteArray;
use sp_consensus_babe as babe_primitives;
use parity_scale_codec::{Decode, Encode};
use polkadot_primitives::{
BlockNumber, CandidateHash, CandidateIndex, CoreIndex, Hash, Header, SessionIndex,
ValidatorIndex, ValidatorSignature,
};
use sp_application_crypto::ByteArray;
/// Validators assigning to check a particular candidate are split up into tranches.
/// Earlier tranches of validators check first, with later tranches serving as backup.
pub type DelayTranche = u32;
/// Validators assigning to check a particular candidate are split up into tranches.
/// Earlier tranches of validators check first, with later tranches serving as backup.
pub type DelayTranche = u32;
/// A static context used to compute the Relay VRF story based on the
/// VRF output included in the header-chain.
pub const RELAY_VRF_STORY_CONTEXT: &[u8] = b"A&V RC-VRF";
/// A static context used to compute the Relay VRF story based on the
/// VRF output included in the header-chain.
pub const RELAY_VRF_STORY_CONTEXT: &[u8] = b"A&V RC-VRF";
/// A static context used for all relay-vrf-modulo VRFs.
pub const RELAY_VRF_MODULO_CONTEXT: &[u8] = b"A&V MOD";
/// A static context used for all relay-vrf-modulo VRFs.
pub const RELAY_VRF_MODULO_CONTEXT: &[u8] = b"A&V MOD";
/// A static context used for all relay-vrf-modulo VRFs.
pub const RELAY_VRF_DELAY_CONTEXT: &[u8] = b"A&V DELAY";
/// A static context used for all relay-vrf-modulo VRFs.
pub const RELAY_VRF_DELAY_CONTEXT: &[u8] = b"A&V DELAY";
/// A static context used for transcripts indicating assigned availability core.
pub const ASSIGNED_CORE_CONTEXT: &[u8] = b"A&V ASSIGNED";
/// A static context used for transcripts indicating assigned availability core.
pub const ASSIGNED_CORE_CONTEXT: &[u8] = b"A&V ASSIGNED";
/// A static context associated with producing randomness for a core.
pub const CORE_RANDOMNESS_CONTEXT: &[u8] = b"A&V CORE";
/// A static context associated with producing randomness for a core.
pub const CORE_RANDOMNESS_CONTEXT: &[u8] = b"A&V CORE";
/// A static context associated with producing randomness for a tranche.
pub const TRANCHE_RANDOMNESS_CONTEXT: &[u8] = b"A&V TRANCHE";
/// A static context associated with producing randomness for a tranche.
pub const TRANCHE_RANDOMNESS_CONTEXT: &[u8] = b"A&V TRANCHE";
/// random bytes derived from the VRF submitted within the block by the
/// block author as a credential and used as input to approval assignment criteria.
#[derive(Debug, Clone, Encode, Decode, PartialEq)]
pub struct RelayVRFStory(pub [u8; 32]);
/// random bytes derived from the VRF submitted within the block by the
/// block author as a credential and used as input to approval assignment criteria.
#[derive(Debug, Clone, Encode, Decode, PartialEq)]
pub struct RelayVRFStory(pub [u8; 32]);
/// Different kinds of input data or criteria that can prove a validator's assignment
/// to check a particular parachain.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum AssignmentCertKind {
/// An assignment story based on the VRF that authorized the relay-chain block where the
/// candidate was included combined with a sample number.
///
/// The context used to produce bytes is [`RELAY_VRF_MODULO_CONTEXT`]
RelayVRFModulo {
/// The sample number used in this cert.
sample: u32,
},
/// An assignment story based on the VRF that authorized the relay-chain block where the
/// candidate was included combined with the index of a particular core.
///
/// The context is [`RELAY_VRF_DELAY_CONTEXT`]
RelayVRFDelay {
/// The core index chosen in this cert.
core_index: CoreIndex,
},
}
/// A certification of assignment.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct AssignmentCert {
/// The criterion which is claimed to be met by this cert.
pub kind: AssignmentCertKind,
/// The VRF signature showing the criterion is met.
pub vrf: VrfSignature,
}
/// An assignment criterion which refers to the candidate under which the assignment is
/// relevant by block hash.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct IndirectAssignmentCert {
/// A block hash where the candidate appears.
pub block_hash: Hash,
/// The validator index.
pub validator: ValidatorIndex,
/// The cert itself.
pub cert: AssignmentCert,
}
/// 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,
/// so this can be transformed into a `SignedApprovalVote`.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct IndirectSignedApprovalVote {
/// A block hash where the candidate appears.
pub block_hash: Hash,
/// The index of the candidate in the list of candidates fully included as-of the block.
pub candidate_index: CandidateIndex,
/// The validator index.
pub validator: ValidatorIndex,
/// The signature by the validator.
pub signature: ValidatorSignature,
}
/// Metadata about a block which is now live in the approval protocol.
#[derive(Debug)]
pub struct BlockApprovalMeta {
/// The hash of the block.
pub hash: Hash,
/// The number of the block.
pub number: BlockNumber,
/// The hash of the parent block.
pub parent_hash: Hash,
/// The candidates included by the block.
/// Note that these are not the same as the candidates that appear within the block body.
pub candidates: Vec<CandidateHash>,
/// The consensus slot of the block.
pub slot: Slot,
/// The session of the block.
pub session: SessionIndex,
}
/// Errors that can occur during the approvals protocol.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum ApprovalError {
#[error("Schnorrkel signature error")]
SchnorrkelSignature(schnorrkel::errors::SignatureError),
#[error("Authority index {0} out of bounds")]
AuthorityOutOfBounds(usize),
}
/// An unsafe VRF output. Provide BABE Epoch info to create a `RelayVRFStory`.
pub struct UnsafeVRFOutput {
vrf_output: VrfOutput,
slot: Slot,
authority_index: u32,
}
impl UnsafeVRFOutput {
/// Get the slot.
pub fn slot(&self) -> Slot {
self.slot
/// Different kinds of input data or criteria that can prove a validator's assignment
/// to check a particular parachain.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum AssignmentCertKind {
/// An assignment story based on the VRF that authorized the relay-chain block where the
/// candidate was included combined with a sample number.
///
/// The context used to produce bytes is [`RELAY_VRF_MODULO_CONTEXT`]
RelayVRFModulo {
/// The sample number used in this cert.
sample: u32,
},
/// An assignment story based on the VRF that authorized the relay-chain block where the
/// candidate was included combined with the index of a particular core.
///
/// The context is [`RELAY_VRF_DELAY_CONTEXT`]
RelayVRFDelay {
/// The core index chosen in this cert.
core_index: CoreIndex,
},
}
/// Compute the randomness associated with this VRF output.
pub fn compute_randomness(
self,
authorities: &[(babe_primitives::AuthorityId, babe_primitives::BabeAuthorityWeight)],
randomness: &babe_primitives::Randomness,
epoch_index: u64,
) -> Result<RelayVRFStory, ApprovalError> {
let author = match authorities.get(self.authority_index as usize) {
None => return Err(ApprovalError::AuthorityOutOfBounds(self.authority_index as _)),
Some(x) => &x.0,
};
/// A certification of assignment.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct AssignmentCert {
/// The criterion which is claimed to be met by this cert.
pub kind: AssignmentCertKind,
/// The VRF signature showing the criterion is met.
pub vrf: VrfSignature,
}
let pubkey = schnorrkel::PublicKey::from_bytes(author.as_slice())
.map_err(ApprovalError::SchnorrkelSignature)?;
/// An assignment criterion which refers to the candidate under which the assignment is
/// relevant by block hash.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct IndirectAssignmentCert {
/// A block hash where the candidate appears.
pub block_hash: Hash,
/// The validator index.
pub validator: ValidatorIndex,
/// The cert itself.
pub cert: AssignmentCert,
}
let transcript = sp_consensus_babe::make_vrf_transcript(randomness, self.slot, epoch_index);
/// 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,
/// so this can be transformed into a `SignedApprovalVote`.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct IndirectSignedApprovalVote {
/// A block hash where the candidate appears.
pub block_hash: Hash,
/// The index of the candidate in the list of candidates fully included as-of the block.
pub candidate_index: CandidateIndex,
/// The validator index.
pub validator: ValidatorIndex,
/// The signature by the validator.
pub signature: ValidatorSignature,
}
let inout = self
.vrf_output
.0
.attach_input_hash(&pubkey, transcript.0)
.map_err(ApprovalError::SchnorrkelSignature)?;
Ok(RelayVRFStory(inout.make_bytes(RELAY_VRF_STORY_CONTEXT)))
/// Metadata about a block which is now live in the approval protocol.
#[derive(Debug)]
pub struct BlockApprovalMeta {
/// The hash of the block.
pub hash: Hash,
/// The number of the block.
pub number: BlockNumber,
/// The hash of the parent block.
pub parent_hash: Hash,
/// The candidates included by the block.
/// Note that these are not the same as the candidates that appear within the block body.
pub candidates: Vec<CandidateHash>,
/// The consensus slot of the block.
pub slot: Slot,
/// The session of the block.
pub session: SessionIndex,
}
/// Errors that can occur during the approvals protocol.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum ApprovalError {
#[error("Schnorrkel signature error")]
SchnorrkelSignature(schnorrkel::errors::SignatureError),
#[error("Authority index {0} out of bounds")]
AuthorityOutOfBounds(usize),
}
/// An unsafe VRF output. Provide BABE Epoch info to create a `RelayVRFStory`.
pub struct UnsafeVRFOutput {
vrf_output: VrfOutput,
slot: Slot,
authority_index: u32,
}
impl UnsafeVRFOutput {
/// Get the slot.
pub fn slot(&self) -> Slot {
self.slot
}
/// Compute the randomness associated with this VRF output.
pub fn compute_randomness(
self,
authorities: &[(babe_primitives::AuthorityId, babe_primitives::BabeAuthorityWeight)],
randomness: &babe_primitives::Randomness,
epoch_index: u64,
) -> Result<RelayVRFStory, ApprovalError> {
let author = match authorities.get(self.authority_index as usize) {
None => return Err(ApprovalError::AuthorityOutOfBounds(self.authority_index as _)),
Some(x) => &x.0,
};
let pubkey = schnorrkel::PublicKey::from_bytes(author.as_slice())
.map_err(ApprovalError::SchnorrkelSignature)?;
let transcript =
sp_consensus_babe::make_vrf_transcript(randomness, self.slot, epoch_index);
let inout = self
.vrf_output
.0
.attach_input_hash(&pubkey, transcript.0)
.map_err(ApprovalError::SchnorrkelSignature)?;
Ok(RelayVRFStory(inout.make_bytes(super::v1::RELAY_VRF_STORY_CONTEXT)))
}
}
/// Extract the slot number and relay VRF from a header.
///
/// This fails if either there is no BABE `PreRuntime` digest or
/// the digest has type `SecondaryPlain`, which Substrate nodes do
/// not produce or accept anymore.
pub fn babe_unsafe_vrf_info(header: &Header) -> Option<UnsafeVRFOutput> {
use babe_primitives::digests::CompatibleDigestItem;
for digest in &header.digest.logs {
if let Some(pre) = digest.as_babe_pre_digest() {
let slot = pre.slot();
let authority_index = pre.authority_index();
return pre.vrf_signature().map(|sig| UnsafeVRFOutput {
vrf_output: sig.output.clone(),
slot,
authority_index,
})
}
}
None
}
}
/// Extract the slot number and relay VRF from a header.
///
/// This fails if either there is no BABE `PreRuntime` digest or
/// the digest has type `SecondaryPlain`, which Substrate nodes do
/// not produce or accept anymore.
pub fn babe_unsafe_vrf_info(header: &Header) -> Option<UnsafeVRFOutput> {
use babe_primitives::digests::CompatibleDigestItem;
/// A list of primitives introduced by v2.
pub mod v2 {
use parity_scale_codec::{Decode, Encode};
pub use sp_consensus_babe::{
Randomness, Slot, VrfOutput, VrfProof, VrfSignature, VrfTranscript,
};
use std::ops::BitOr;
for digest in &header.digest.logs {
if let Some(pre) = digest.as_babe_pre_digest() {
let slot = pre.slot();
let authority_index = pre.authority_index();
use bitvec::{prelude::Lsb0, vec::BitVec};
use polkadot_primitives::{CandidateIndex, CoreIndex, Hash, ValidatorIndex};
return pre.vrf_signature().map(|sig| UnsafeVRFOutput {
vrf_output: sig.output.clone(),
slot,
authority_index,
/// A static context associated with producing randomness for a core.
pub const CORE_RANDOMNESS_CONTEXT: &[u8] = b"A&V CORE v2";
/// A static context associated with producing randomness for v2 multi-core assignments.
pub const ASSIGNED_CORE_CONTEXT: &[u8] = b"A&V ASSIGNED v2";
/// A static context used for all relay-vrf-modulo VRFs for v2 multi-core assignments.
pub const RELAY_VRF_MODULO_CONTEXT: &[u8] = b"A&V MOD v2";
/// A read-only bitvec wrapper
#[derive(Clone, Debug, Encode, Decode, Hash, PartialEq, Eq)]
pub struct Bitfield<T>(BitVec<u8, bitvec::order::Lsb0>, std::marker::PhantomData<T>);
/// A `read-only`, `non-zero` bitfield.
/// Each 1 bit identifies a candidate by the bitfield bit index.
pub type CandidateBitfield = Bitfield<CandidateIndex>;
/// A bitfield of core assignments.
pub type CoreBitfield = Bitfield<CoreIndex>;
/// Errors that can occur when creating and manipulating bitfields.
#[derive(Debug)]
pub enum BitfieldError {
/// All bits are zero.
NullAssignment,
}
/// A bit index in `Bitfield`.
#[cfg_attr(test, derive(PartialEq, Clone))]
pub struct BitIndex(pub usize);
/// Helper trait to convert primitives to `BitIndex`.
pub trait AsBitIndex {
/// Returns the index of the corresponding bit in `Bitfield`.
fn as_bit_index(&self) -> BitIndex;
}
impl<T> Bitfield<T> {
/// Returns the bit value at specified `index`. If `index` is greater than bitfield size,
/// returns `false`.
pub fn bit_at(&self, index: BitIndex) -> bool {
if self.0.len() <= index.0 {
false
} else {
self.0[index.0]
}
}
/// Returns number of bits.
pub fn len(&self) -> usize {
self.0.len()
}
/// Returns the number of 1 bits.
pub fn count_ones(&self) -> usize {
self.0.count_ones()
}
/// Returns the index of the first 1 bit.
pub fn first_one(&self) -> Option<usize> {
self.0.first_one()
}
/// Returns an iterator over inner bits.
pub fn iter_ones(&self) -> bitvec::slice::IterOnes<u8, bitvec::order::Lsb0> {
self.0.iter_ones()
}
/// For testing purpose, we want a inner mutable ref.
#[cfg(test)]
pub fn inner_mut(&mut self) -> &mut BitVec<u8, bitvec::order::Lsb0> {
&mut self.0
}
/// Returns the inner bitfield and consumes `self`.
pub fn into_inner(self) -> BitVec<u8, bitvec::order::Lsb0> {
self.0
}
}
impl AsBitIndex for CandidateIndex {
fn as_bit_index(&self) -> BitIndex {
BitIndex(*self as usize)
}
}
impl AsBitIndex for CoreIndex {
fn as_bit_index(&self) -> BitIndex {
BitIndex(self.0 as usize)
}
}
impl AsBitIndex for usize {
fn as_bit_index(&self) -> BitIndex {
BitIndex(*self)
}
}
impl<T> From<T> for Bitfield<T>
where
T: AsBitIndex,
{
fn from(value: T) -> Self {
Self(
{
let mut bv = bitvec::bitvec![u8, Lsb0; 0; value.as_bit_index().0 + 1];
bv.set(value.as_bit_index().0, true);
bv
},
Default::default(),
)
}
}
impl<T> TryFrom<Vec<T>> for Bitfield<T>
where
T: Into<Bitfield<T>>,
{
type Error = BitfieldError;
fn try_from(mut value: Vec<T>) -> Result<Self, Self::Error> {
if value.is_empty() {
return Err(BitfieldError::NullAssignment)
}
let initial_bitfield =
value.pop().expect("Just checked above it's not empty; qed").into();
Ok(Self(
value.into_iter().fold(initial_bitfield.0, |initial_bitfield, element| {
let mut bitfield: Bitfield<T> = element.into();
bitfield
.0
.resize(std::cmp::max(initial_bitfield.len(), bitfield.0.len()), false);
bitfield.0.bitor(initial_bitfield)
}),
Default::default(),
))
}
}
/// Certificate is changed compared to `AssignmentCertKind`:
/// - introduced RelayVRFModuloCompact
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub enum AssignmentCertKindV2 {
/// Multiple assignment stories based on the VRF that authorized the relay-chain block
/// where the candidates were included.
///
/// The context is [`super::v2::RELAY_VRF_MODULO_CONTEXT`]
#[codec(index = 0)]
RelayVRFModuloCompact {
/// A bitfield representing the core indices claimed by this assignment.
core_bitfield: CoreBitfield,
},
/// An assignment story based on the VRF that authorized the relay-chain block where the
/// candidate was included combined with the index of a particular core.
///
/// The context is [`super::v1::RELAY_VRF_DELAY_CONTEXT`]
#[codec(index = 1)]
RelayVRFDelay {
/// The core index chosen in this cert.
core_index: CoreIndex,
},
/// Deprectated assignment. Soon to be removed.
/// An assignment story based on the VRF that authorized the relay-chain block where the
/// candidate was included combined with a sample number.
///
/// The context used to produce bytes is [`super::v1::RELAY_VRF_MODULO_CONTEXT`]
#[codec(index = 2)]
RelayVRFModulo {
/// The sample number used in this cert.
sample: u32,
},
}
/// A certification of assignment.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct AssignmentCertV2 {
/// The criterion which is claimed to be met by this cert.
pub kind: AssignmentCertKindV2,
/// The VRF showing the criterion is met.
pub vrf: VrfSignature,
}
impl From<super::v1::AssignmentCert> for AssignmentCertV2 {
fn from(cert: super::v1::AssignmentCert) -> Self {
Self {
kind: match cert.kind {
super::v1::AssignmentCertKind::RelayVRFDelay { core_index } =>
AssignmentCertKindV2::RelayVRFDelay { core_index },
super::v1::AssignmentCertKind::RelayVRFModulo { sample } =>
AssignmentCertKindV2::RelayVRFModulo { sample },
},
vrf: cert.vrf,
}
}
}
/// Errors that can occur when trying to convert to/from assignment v1/v2
#[derive(Debug)]
pub enum AssignmentConversionError {
/// Assignment certificate is not supported in v1.
CertificateNotSupported,
}
impl TryFrom<AssignmentCertV2> for super::v1::AssignmentCert {
type Error = AssignmentConversionError;
fn try_from(cert: AssignmentCertV2) -> Result<Self, AssignmentConversionError> {
Ok(Self {
kind: match cert.kind {
AssignmentCertKindV2::RelayVRFDelay { core_index } =>
super::v1::AssignmentCertKind::RelayVRFDelay { core_index },
AssignmentCertKindV2::RelayVRFModulo { sample } =>
super::v1::AssignmentCertKind::RelayVRFModulo { sample },
// Not supported
_ => return Err(AssignmentConversionError::CertificateNotSupported),
},
vrf: cert.vrf,
})
}
}
None
/// An assignment criterion which refers to the candidate under which the assignment is
/// relevant by block hash.
#[derive(Debug, Clone, Encode, Decode, PartialEq, Eq)]
pub struct IndirectAssignmentCertV2 {
/// A block hash where the candidate appears.
pub block_hash: Hash,
/// The validator index.
pub validator: ValidatorIndex,
/// The cert itself.
pub cert: AssignmentCertV2,
}
impl From<super::v1::IndirectAssignmentCert> for IndirectAssignmentCertV2 {
fn from(indirect_cert: super::v1::IndirectAssignmentCert) -> Self {
Self {
block_hash: indirect_cert.block_hash,
validator: indirect_cert.validator,
cert: indirect_cert.cert.into(),
}
}
}
impl TryFrom<IndirectAssignmentCertV2> for super::v1::IndirectAssignmentCert {
type Error = AssignmentConversionError;
fn try_from(
indirect_cert: IndirectAssignmentCertV2,
) -> Result<Self, AssignmentConversionError> {
Ok(Self {
block_hash: indirect_cert.block_hash,
validator: indirect_cert.validator,
cert: indirect_cert.cert.try_into()?,
})
}
}
}
#[cfg(test)]
mod test {
use super::v2::{BitIndex, Bitfield};
use polkadot_primitives::{CandidateIndex, CoreIndex};
#[test]
fn test_assignment_bitfield_from_vec() {
let candidate_indices = vec![1u32, 7, 3, 10, 45, 8, 200, 2];
let max_index = *candidate_indices.iter().max().unwrap();
let bitfield = Bitfield::try_from(candidate_indices.clone()).unwrap();
let candidate_indices =
candidate_indices.into_iter().map(|i| BitIndex(i as usize)).collect::<Vec<_>>();
// Test 1 bits.
for index in candidate_indices.clone() {
assert!(bitfield.bit_at(index));
}
// Test 0 bits.
for index in 0..max_index {
if candidate_indices.contains(&BitIndex(index as usize)) {
continue
}
assert!(!bitfield.bit_at(BitIndex(index as usize)));
}
}
#[test]
fn test_assignment_bitfield_invariant_msb() {
let core_indices = vec![CoreIndex(1), CoreIndex(3), CoreIndex(10), CoreIndex(20)];
let mut bitfield = Bitfield::try_from(core_indices.clone()).unwrap();
assert!(bitfield.inner_mut().pop().unwrap());
for i in 0..1024 {
assert!(Bitfield::try_from(CoreIndex(i)).unwrap().inner_mut().pop().unwrap());
assert!(Bitfield::try_from(i).unwrap().inner_mut().pop().unwrap());
}
}
#[test]
fn test_assignment_bitfield_basic() {
let bitfield = Bitfield::try_from(CoreIndex(0)).unwrap();
assert!(bitfield.bit_at(BitIndex(0)));
assert!(!bitfield.bit_at(BitIndex(1)));
assert_eq!(bitfield.len(), 1);
let mut bitfield = Bitfield::try_from(20 as CandidateIndex).unwrap();
assert!(bitfield.bit_at(BitIndex(20)));
assert_eq!(bitfield.inner_mut().count_ones(), 1);
assert_eq!(bitfield.len(), 21);
}
}
+5
View File
@@ -139,6 +139,7 @@ polkadot-statement-distribution = { path = "../network/statement-distribution",
[dev-dependencies]
polkadot-test-client = { path = "../test/client" }
polkadot-node-subsystem-test-helpers = { path = "../subsystem-test-helpers" }
test-helpers = { package = "polkadot-primitives-test-helpers", path = "../../primitives/test-helpers" }
env_logger = "0.9.0"
assert_matches = "1.5.0"
serial_test = "2.0.0"
@@ -222,3 +223,7 @@ runtime-metrics = [
"rococo-runtime?/runtime-metrics",
"westend-runtime?/runtime-metrics",
]
network-protocol-staging = [
"polkadot-node-network-protocol/network-protocol-staging",
]
+22 -14
View File
@@ -41,7 +41,15 @@ pub(crate) mod columns {
pub const COL_SESSION_WINDOW_DATA: u32 = 5;
}
// Version 4 only changed structures in approval voting, so we can re-export the v4 definitions.
pub mod v3 {
pub use super::v4::{
COL_APPROVAL_DATA, COL_AVAILABILITY_DATA, COL_AVAILABILITY_META,
COL_CHAIN_SELECTION_DATA, COL_DISPUTE_COORDINATOR_DATA, NUM_COLUMNS, ORDERED_COL,
};
}
pub mod v4 {
pub const NUM_COLUMNS: u32 = 5;
pub const COL_AVAILABILITY_DATA: u32 = 0;
pub const COL_AVAILABILITY_META: u32 = 1;
@@ -73,14 +81,14 @@ pub struct ColumnsConfig {
/// The real columns used by the parachains DB.
#[cfg(any(test, feature = "full-node"))]
pub const REAL_COLUMNS: ColumnsConfig = ColumnsConfig {
col_availability_data: columns::v3::COL_AVAILABILITY_DATA,
col_availability_meta: columns::v3::COL_AVAILABILITY_META,
col_approval_data: columns::v3::COL_APPROVAL_DATA,
col_chain_selection_data: columns::v3::COL_CHAIN_SELECTION_DATA,
col_dispute_coordinator_data: columns::v3::COL_DISPUTE_COORDINATOR_DATA,
col_availability_data: columns::v4::COL_AVAILABILITY_DATA,
col_availability_meta: columns::v4::COL_AVAILABILITY_META,
col_approval_data: columns::v4::COL_APPROVAL_DATA,
col_chain_selection_data: columns::v4::COL_CHAIN_SELECTION_DATA,
col_dispute_coordinator_data: columns::v4::COL_DISPUTE_COORDINATOR_DATA,
};
#[derive(PartialEq)]
#[derive(PartialEq, Copy, Clone)]
pub(crate) enum DatabaseKind {
ParityDB,
RocksDB,
@@ -125,28 +133,28 @@ pub fn open_creating_rocksdb(
let path = root.join("parachains").join("db");
let mut db_config = DatabaseConfig::with_columns(columns::v3::NUM_COLUMNS);
let mut db_config = DatabaseConfig::with_columns(columns::v4::NUM_COLUMNS);
let _ = db_config
.memory_budget
.insert(columns::v3::COL_AVAILABILITY_DATA, cache_sizes.availability_data);
.insert(columns::v4::COL_AVAILABILITY_DATA, cache_sizes.availability_data);
let _ = db_config
.memory_budget
.insert(columns::v3::COL_AVAILABILITY_META, cache_sizes.availability_meta);
.insert(columns::v4::COL_AVAILABILITY_META, cache_sizes.availability_meta);
let _ = db_config
.memory_budget
.insert(columns::v3::COL_APPROVAL_DATA, cache_sizes.approval_data);
.insert(columns::v4::COL_APPROVAL_DATA, cache_sizes.approval_data);
let path_str = path
.to_str()
.ok_or_else(|| other_io_error(format!("Bad database path: {:?}", path)))?;
std::fs::create_dir_all(&path_str)?;
upgrade::try_upgrade_db(&path, DatabaseKind::RocksDB)?;
upgrade::try_upgrade_db(&path, DatabaseKind::RocksDB, upgrade::CURRENT_VERSION)?;
let db = Database::open(&db_config, &path_str)?;
let db = polkadot_node_subsystem_util::database::kvdb_impl::DbAdapter::new(
db,
columns::v3::ORDERED_COL,
columns::v4::ORDERED_COL,
);
Ok(Arc::new(db))
@@ -164,14 +172,14 @@ pub fn open_creating_paritydb(
.ok_or_else(|| other_io_error(format!("Bad database path: {:?}", path)))?;
std::fs::create_dir_all(&path_str)?;
upgrade::try_upgrade_db(&path, DatabaseKind::ParityDB)?;
upgrade::try_upgrade_db(&path, DatabaseKind::ParityDB, upgrade::CURRENT_VERSION)?;
let db = parity_db::Db::open_or_create(&upgrade::paritydb_version_3_config(&path))
.map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))?;
let db = polkadot_node_subsystem_util::database::paritydb_impl::DbAdapter::new(
db,
columns::v3::ORDERED_COL,
columns::v4::ORDERED_COL,
);
Ok(Arc::new(db))
}
@@ -22,13 +22,17 @@ use std::{
str::FromStr,
};
use polkadot_node_core_approval_voting::approval_db::v2::{
migration_helpers::v1_to_v2, Config as ApprovalDbConfig,
};
type Version = u32;
/// Version file name.
const VERSION_FILE_NAME: &'static str = "parachain_db_version";
/// Current db version.
const CURRENT_VERSION: Version = 3;
/// Version 4 changes approval db format for `OurAssignment`.
pub(crate) const CURRENT_VERSION: Version = 4;
#[derive(thiserror::Error, Debug)]
pub enum Error {
@@ -38,6 +42,10 @@ pub enum Error {
CorruptedVersionFile,
#[error("Parachains DB has a future version (expected {current:?}, found {got:?})")]
FutureVersion { current: Version, got: Version },
#[error("Parachain DB migration failed")]
MigrationFailed,
#[error("Parachain DB migration would take forever")]
MigrationLoop,
}
impl From<Error> for io::Error {
@@ -49,10 +57,42 @@ impl From<Error> for io::Error {
}
}
/// Try upgrading parachain's database to the current version.
pub(crate) fn try_upgrade_db(db_path: &Path, db_kind: DatabaseKind) -> Result<(), Error> {
/// Try upgrading parachain's database to a target version.
pub(crate) fn try_upgrade_db(
db_path: &Path,
db_kind: DatabaseKind,
target_version: Version,
) -> Result<(), Error> {
// Ensure we don't loop forever below because of a bug.
const MAX_MIGRATIONS: u32 = 30;
#[cfg(test)]
remove_file_lock(&db_path);
// Loop migrations until we reach the target version.
for _ in 0..MAX_MIGRATIONS {
let version = try_upgrade_db_to_next_version(db_path, db_kind)?;
#[cfg(test)]
remove_file_lock(&db_path);
if version == target_version {
return Ok(())
}
}
Err(Error::MigrationLoop)
}
/// Try upgrading parachain's database to the next version.
/// If successfull, it returns the current version.
pub(crate) fn try_upgrade_db_to_next_version(
db_path: &Path,
db_kind: DatabaseKind,
) -> Result<Version, Error> {
let is_empty = db_path.read_dir().map_or(true, |mut d| d.next().is_none());
if !is_empty {
let new_version = if !is_empty {
match get_db_version(db_path)? {
// 0 -> 1 migration
Some(0) => migrate_from_version_0_to_1(db_path, db_kind)?,
@@ -60,21 +100,26 @@ pub(crate) fn try_upgrade_db(db_path: &Path, db_kind: DatabaseKind) -> Result<()
Some(1) => migrate_from_version_1_to_2(db_path, db_kind)?,
// 2 -> 3 migration
Some(2) => migrate_from_version_2_to_3(db_path, db_kind)?,
// 3 -> 4 migration
Some(3) => migrate_from_version_3_to_4(db_path, db_kind)?,
// Already at current version, do nothing.
Some(CURRENT_VERSION) => (),
Some(CURRENT_VERSION) => CURRENT_VERSION,
// This is an arbitrary future version, we don't handle it.
Some(v) => return Err(Error::FutureVersion { current: CURRENT_VERSION, got: v }),
// No version file. For `RocksDB` we dont need to do anything.
None if db_kind == DatabaseKind::RocksDB => (),
None if db_kind == DatabaseKind::RocksDB => CURRENT_VERSION,
// No version file. `ParityDB` did not previously have a version defined.
// We handle this as a `0 -> 1` migration.
None if db_kind == DatabaseKind::ParityDB =>
migrate_from_version_0_to_1(db_path, db_kind)?,
None => unreachable!(),
}
}
} else {
CURRENT_VERSION
};
update_version(db_path)
update_version(db_path, new_version)?;
Ok(new_version)
}
/// Reads current database version from the file at given path.
@@ -91,9 +136,9 @@ fn get_db_version(path: &Path) -> Result<Option<Version>, Error> {
/// Writes current database version to the file.
/// Creates a new file if the version file does not exist yet.
fn update_version(path: &Path) -> Result<(), Error> {
fn update_version(path: &Path, new_version: Version) -> Result<(), Error> {
fs::create_dir_all(path)?;
fs::write(version_file_path(path), CURRENT_VERSION.to_string()).map_err(Into::into)
fs::write(version_file_path(path), new_version.to_string()).map_err(Into::into)
}
/// Returns the version file path.
@@ -103,7 +148,7 @@ fn version_file_path(path: &Path) -> PathBuf {
file_path
}
fn migrate_from_version_0_to_1(path: &Path, db_kind: DatabaseKind) -> Result<(), Error> {
fn migrate_from_version_0_to_1(path: &Path, db_kind: DatabaseKind) -> Result<Version, Error> {
gum::info!(target: LOG_TARGET, "Migrating parachains db from version 0 to version 1 ...");
match db_kind {
@@ -116,7 +161,7 @@ fn migrate_from_version_0_to_1(path: &Path, db_kind: DatabaseKind) -> Result<(),
})
}
fn migrate_from_version_1_to_2(path: &Path, db_kind: DatabaseKind) -> Result<(), Error> {
fn migrate_from_version_1_to_2(path: &Path, db_kind: DatabaseKind) -> Result<Version, Error> {
gum::info!(target: LOG_TARGET, "Migrating parachains db from version 1 to version 2 ...");
match db_kind {
@@ -129,7 +174,48 @@ fn migrate_from_version_1_to_2(path: &Path, db_kind: DatabaseKind) -> Result<(),
})
}
fn migrate_from_version_2_to_3(path: &Path, db_kind: DatabaseKind) -> Result<(), Error> {
// Migrade approval voting database. `OurAssignment` has been changed to support the v2 assignments.
// As these are backwards compatible, we'll convert the old entries in the new format.
fn migrate_from_version_3_to_4(path: &Path, db_kind: DatabaseKind) -> Result<Version, Error> {
gum::info!(target: LOG_TARGET, "Migrating parachains db from version 3 to version 4 ...");
use polkadot_node_subsystem_util::database::{
kvdb_impl::DbAdapter as RocksDbAdapter, paritydb_impl::DbAdapter as ParityDbAdapter,
};
use std::sync::Arc;
let approval_db_config =
ApprovalDbConfig { col_approval_data: super::REAL_COLUMNS.col_approval_data };
let _result = match db_kind {
DatabaseKind::ParityDB => {
let db = ParityDbAdapter::new(
parity_db::Db::open(&paritydb_version_3_config(path))
.map_err(|e| other_io_error(format!("Error opening db {:?}", e)))?,
super::columns::v3::ORDERED_COL,
);
v1_to_v2(Arc::new(db), approval_db_config).map_err(|_| Error::MigrationFailed)?;
},
DatabaseKind::RocksDB => {
let db_path = path
.to_str()
.ok_or_else(|| super::other_io_error("Invalid database path".into()))?;
let db_cfg =
kvdb_rocksdb::DatabaseConfig::with_columns(super::columns::v3::NUM_COLUMNS);
let db = RocksDbAdapter::new(
kvdb_rocksdb::Database::open(&db_cfg, db_path)?,
&super::columns::v3::ORDERED_COL,
);
v1_to_v2(Arc::new(db), approval_db_config).map_err(|_| Error::MigrationFailed)?;
},
};
gum::info!(target: LOG_TARGET, "Migration complete! ");
Ok(CURRENT_VERSION)
}
fn migrate_from_version_2_to_3(path: &Path, db_kind: DatabaseKind) -> Result<Version, Error> {
gum::info!(target: LOG_TARGET, "Migrating parachains db from version 2 to version 3 ...");
match db_kind {
DatabaseKind::ParityDB => paritydb_migrate_from_version_2_to_3(path),
@@ -143,7 +229,7 @@ fn migrate_from_version_2_to_3(path: &Path, db_kind: DatabaseKind) -> Result<(),
/// Migration from version 0 to version 1:
/// * the number of columns has changed from 3 to 5;
fn rocksdb_migrate_from_version_0_to_1(path: &Path) -> Result<(), Error> {
fn rocksdb_migrate_from_version_0_to_1(path: &Path) -> Result<Version, Error> {
use kvdb_rocksdb::{Database, DatabaseConfig};
let db_path = path
@@ -155,12 +241,12 @@ fn rocksdb_migrate_from_version_0_to_1(path: &Path) -> Result<(), Error> {
db.add_column()?;
db.add_column()?;
Ok(())
Ok(1)
}
/// Migration from version 1 to version 2:
/// * the number of columns has changed from 5 to 6;
fn rocksdb_migrate_from_version_1_to_2(path: &Path) -> Result<(), Error> {
fn rocksdb_migrate_from_version_1_to_2(path: &Path) -> Result<Version, Error> {
use kvdb_rocksdb::{Database, DatabaseConfig};
let db_path = path
@@ -171,10 +257,10 @@ fn rocksdb_migrate_from_version_1_to_2(path: &Path) -> Result<(), Error> {
db.add_column()?;
Ok(())
Ok(2)
}
fn rocksdb_migrate_from_version_2_to_3(path: &Path) -> Result<(), Error> {
fn rocksdb_migrate_from_version_2_to_3(path: &Path) -> Result<Version, Error> {
use kvdb_rocksdb::{Database, DatabaseConfig};
let db_path = path
@@ -185,7 +271,7 @@ fn rocksdb_migrate_from_version_2_to_3(path: &Path) -> Result<(), Error> {
db.remove_last_column()?;
Ok(())
Ok(3)
}
// This currently clears columns which had their configs altered between versions.
@@ -249,7 +335,7 @@ fn paritydb_fix_columns(
pub(crate) fn paritydb_version_1_config(path: &Path) -> parity_db::Options {
let mut options =
parity_db::Options::with_columns(&path, super::columns::v1::NUM_COLUMNS as u8);
for i in columns::v3::ORDERED_COL {
for i in columns::v4::ORDERED_COL {
options.columns[*i as usize].btree_index = true;
}
@@ -260,7 +346,7 @@ pub(crate) fn paritydb_version_1_config(path: &Path) -> parity_db::Options {
pub(crate) fn paritydb_version_2_config(path: &Path) -> parity_db::Options {
let mut options =
parity_db::Options::with_columns(&path, super::columns::v2::NUM_COLUMNS as u8);
for i in columns::v3::ORDERED_COL {
for i in columns::v4::ORDERED_COL {
options.columns[*i as usize].btree_index = true;
}
@@ -278,48 +364,161 @@ pub(crate) fn paritydb_version_3_config(path: &Path) -> parity_db::Options {
options
}
/// Database configuration for version 0. This is useful just for testing.
#[cfg(test)]
pub(crate) fn paritydb_version_0_config(path: &Path) -> parity_db::Options {
let mut options =
parity_db::Options::with_columns(&path, super::columns::v0::NUM_COLUMNS as u8);
options.columns[super::columns::v4::COL_AVAILABILITY_META as usize].btree_index = true;
options
}
/// Migration from version 0 to version 1.
/// Cases covered:
/// - upgrading from v0.9.23 or earlier -> the `dispute coordinator column` was changed
/// - upgrading from v0.9.24+ -> this is a no op assuming the DB has been manually fixed as per
/// release notes
fn paritydb_migrate_from_version_0_to_1(path: &Path) -> Result<(), Error> {
fn paritydb_migrate_from_version_0_to_1(path: &Path) -> Result<Version, Error> {
// Delete the `dispute coordinator` column if needed (if column configuration is changed).
paritydb_fix_columns(
path,
paritydb_version_1_config(path),
vec![super::columns::v3::COL_DISPUTE_COORDINATOR_DATA],
vec![super::columns::v4::COL_DISPUTE_COORDINATOR_DATA],
)?;
Ok(())
Ok(1)
}
/// Migration from version 1 to version 2:
/// - add a new column for session information storage
fn paritydb_migrate_from_version_1_to_2(path: &Path) -> Result<(), Error> {
fn paritydb_migrate_from_version_1_to_2(path: &Path) -> Result<Version, Error> {
let mut options = paritydb_version_1_config(path);
// Adds the session info column.
parity_db::Db::add_column(&mut options, Default::default())
.map_err(|e| other_io_error(format!("Error adding column {:?}", e)))?;
Ok(())
Ok(2)
}
/// Migration from version 2 to version 3:
/// - drop the column used by `RollingSessionWindow`
fn paritydb_migrate_from_version_2_to_3(path: &Path) -> Result<(), Error> {
fn paritydb_migrate_from_version_2_to_3(path: &Path) -> Result<Version, Error> {
parity_db::Db::drop_last_column(&mut paritydb_version_2_config(path))
.map_err(|e| other_io_error(format!("Error removing COL_SESSION_WINDOW_DATA {:?}", e)))?;
Ok(())
Ok(3)
}
/// Remove the lock file. If file is locked, it will wait up to 1s.
#[cfg(test)]
pub fn remove_file_lock(path: &std::path::Path) {
use std::{io::ErrorKind, thread::sleep, time::Duration};
let mut lock_path = std::path::PathBuf::from(path);
lock_path.push("lock");
for _ in 0..10 {
let result = std::fs::remove_file(lock_path.as_path());
match result {
Err(error) => match error.kind() {
ErrorKind::WouldBlock => {
sleep(Duration::from_millis(100));
continue
},
_ => return,
},
Ok(_) => {},
}
}
unreachable!("Database is locked, waited 1s for lock file: {:?}", lock_path);
}
#[cfg(test)]
mod tests {
use super::{
columns::{v2::COL_SESSION_WINDOW_DATA, v3::*},
columns::{v2::COL_SESSION_WINDOW_DATA, v4::*},
*,
};
use polkadot_node_core_approval_voting::approval_db::v2::migration_helpers::v1_to_v2_fill_test_data;
use test_helpers::dummy_candidate_receipt;
#[test]
fn test_paritydb_migrate_0_to_1() {
use parity_db::Db;
let db_dir = tempfile::tempdir().unwrap();
let path = db_dir.path();
{
let db = Db::open_or_create(&paritydb_version_0_config(&path)).unwrap();
db.commit(vec![(
COL_AVAILABILITY_META as u8,
b"5678".to_vec(),
Some(b"somevalue".to_vec()),
)])
.unwrap();
}
try_upgrade_db(&path, DatabaseKind::ParityDB, 1).unwrap();
let db = Db::open(&paritydb_version_1_config(&path)).unwrap();
assert_eq!(
db.get(COL_AVAILABILITY_META as u8, b"5678").unwrap(),
Some("somevalue".as_bytes().to_vec())
);
}
#[test]
fn test_paritydb_migrate_1_to_2() {
use parity_db::Db;
let db_dir = tempfile::tempdir().unwrap();
let path = db_dir.path();
// We need to properly set db version for upgrade to work.
fs::write(version_file_path(path), "1").expect("Failed to write DB version");
{
let db = Db::open_or_create(&paritydb_version_1_config(&path)).unwrap();
// Write some dummy data
db.commit(vec![(
COL_DISPUTE_COORDINATOR_DATA as u8,
b"1234".to_vec(),
Some(b"somevalue".to_vec()),
)])
.unwrap();
assert_eq!(db.num_columns(), columns::v1::NUM_COLUMNS as u8);
}
try_upgrade_db(&path, DatabaseKind::ParityDB, 2).unwrap();
let db = Db::open(&paritydb_version_2_config(&path)).unwrap();
assert_eq!(db.num_columns(), columns::v2::NUM_COLUMNS as u8);
assert_eq!(
db.get(COL_DISPUTE_COORDINATOR_DATA as u8, b"1234").unwrap(),
Some("somevalue".as_bytes().to_vec())
);
// Test we can write the new column.
db.commit(vec![(
COL_SESSION_WINDOW_DATA as u8,
b"1337".to_vec(),
Some(b"0xdeadb00b".to_vec()),
)])
.unwrap();
// Read back data from new column.
assert_eq!(
db.get(COL_SESSION_WINDOW_DATA as u8, b"1337").unwrap(),
Some("0xdeadb00b".as_bytes().to_vec())
);
}
#[test]
fn test_rocksdb_migrate_1_to_2() {
@@ -338,7 +537,7 @@ mod tests {
// We need to properly set db version for upgrade to work.
fs::write(version_file_path(db_dir.path()), "1").expect("Failed to write DB version");
{
let db = DbAdapter::new(db, columns::v3::ORDERED_COL);
let db = DbAdapter::new(db, columns::v4::ORDERED_COL);
db.write(DBTransaction {
ops: vec![DBOp::Insert {
col: COL_DISPUTE_COORDINATOR_DATA,
@@ -349,14 +548,14 @@ mod tests {
.unwrap();
}
try_upgrade_db(&db_dir.path(), DatabaseKind::RocksDB).unwrap();
try_upgrade_db(&db_dir.path(), DatabaseKind::RocksDB, 2).unwrap();
let db_cfg = DatabaseConfig::with_columns(super::columns::v2::NUM_COLUMNS);
let db = Database::open(&db_cfg, db_path).unwrap();
assert_eq!(db.num_columns(), super::columns::v2::NUM_COLUMNS);
let db = DbAdapter::new(db, columns::v3::ORDERED_COL);
let db = DbAdapter::new(db, columns::v4::ORDERED_COL);
assert_eq!(
db.get(COL_DISPUTE_COORDINATOR_DATA, b"1234").unwrap(),
@@ -380,6 +579,109 @@ mod tests {
);
}
#[test]
fn test_migrate_3_to_4() {
use kvdb_rocksdb::{Database, DatabaseConfig};
use polkadot_node_core_approval_voting::approval_db::v2::migration_helpers::v1_to_v2_sanity_check;
use polkadot_node_subsystem_util::database::kvdb_impl::DbAdapter;
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().to_str().unwrap();
let db_cfg: DatabaseConfig = DatabaseConfig::with_columns(super::columns::v3::NUM_COLUMNS);
let approval_cfg = ApprovalDbConfig {
col_approval_data: crate::parachains_db::REAL_COLUMNS.col_approval_data,
};
// We need to properly set db version for upgrade to work.
fs::write(version_file_path(db_dir.path()), "3").expect("Failed to write DB version");
let expected_candidates = {
let db = Database::open(&db_cfg, db_path).unwrap();
assert_eq!(db.num_columns(), super::columns::v3::NUM_COLUMNS as u32);
let db = DbAdapter::new(db, columns::v3::ORDERED_COL);
// Fill the approval voting column with test data.
v1_to_v2_fill_test_data(std::sync::Arc::new(db), approval_cfg, dummy_candidate_receipt)
.unwrap()
};
try_upgrade_db(&db_dir.path(), DatabaseKind::RocksDB, 4).unwrap();
let db_cfg = DatabaseConfig::with_columns(super::columns::v4::NUM_COLUMNS);
let db = Database::open(&db_cfg, db_path).unwrap();
let db = DbAdapter::new(db, columns::v4::ORDERED_COL);
v1_to_v2_sanity_check(std::sync::Arc::new(db), approval_cfg, expected_candidates).unwrap();
}
#[test]
fn test_rocksdb_migrate_0_to_4() {
use kvdb_rocksdb::{Database, DatabaseConfig};
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().to_str().unwrap();
fs::write(version_file_path(db_dir.path()), "0").expect("Failed to write DB version");
try_upgrade_db(&db_dir.path(), DatabaseKind::RocksDB, 4).unwrap();
let db_cfg = DatabaseConfig::with_columns(super::columns::v4::NUM_COLUMNS);
let db = Database::open(&db_cfg, db_path).unwrap();
assert_eq!(db.num_columns(), columns::v4::NUM_COLUMNS);
}
#[test]
fn test_paritydb_migrate_0_to_4() {
use parity_db::Db;
let db_dir = tempfile::tempdir().unwrap();
let path = db_dir.path();
// We need to properly set db version for upgrade to work.
fs::write(version_file_path(path), "0").expect("Failed to write DB version");
{
let db = Db::open_or_create(&paritydb_version_0_config(&path)).unwrap();
assert_eq!(db.num_columns(), columns::v0::NUM_COLUMNS as u8);
}
try_upgrade_db(&path, DatabaseKind::ParityDB, 4).unwrap();
let db = Db::open(&paritydb_version_3_config(&path)).unwrap();
assert_eq!(db.num_columns(), columns::v4::NUM_COLUMNS as u8);
}
#[test]
fn test_paritydb_migrate_2_to_3() {
use parity_db::Db;
let db_dir = tempfile::tempdir().unwrap();
let path = db_dir.path();
let test_key = b"1337";
// We need to properly set db version for upgrade to work.
fs::write(version_file_path(path), "2").expect("Failed to write DB version");
{
let db = Db::open_or_create(&paritydb_version_2_config(&path)).unwrap();
// Write some dummy data
db.commit(vec![(
COL_SESSION_WINDOW_DATA as u8,
test_key.to_vec(),
Some(b"0xdeadb00b".to_vec()),
)])
.unwrap();
assert_eq!(db.num_columns(), columns::v2::NUM_COLUMNS as u8);
}
try_upgrade_db(&path, DatabaseKind::ParityDB, 3).unwrap();
let db = Db::open(&paritydb_version_3_config(&path)).unwrap();
assert_eq!(db.num_columns(), columns::v3::NUM_COLUMNS as u8);
}
#[test]
fn test_rocksdb_migrate_2_to_3() {
use kvdb_rocksdb::{Database, DatabaseConfig};
@@ -387,6 +689,7 @@ mod tests {
let db_dir = tempfile::tempdir().unwrap();
let db_path = db_dir.path().to_str().unwrap();
let db_cfg = DatabaseConfig::with_columns(super::columns::v2::NUM_COLUMNS);
{
let db = Database::open(&db_cfg, db_path).unwrap();
assert_eq!(db.num_columns(), super::columns::v2::NUM_COLUMNS as u32);
@@ -395,7 +698,7 @@ mod tests {
// We need to properly set db version for upgrade to work.
fs::write(version_file_path(db_dir.path()), "2").expect("Failed to write DB version");
try_upgrade_db(&db_dir.path(), DatabaseKind::RocksDB).unwrap();
try_upgrade_db(&db_dir.path(), DatabaseKind::RocksDB, 3).unwrap();
let db_cfg = DatabaseConfig::with_columns(super::columns::v3::NUM_COLUMNS);
let db = Database::open(&db_cfg, db_path).unwrap();
+1 -1
View File
@@ -17,7 +17,7 @@
use super::{relay_chain_selection::*, *};
use futures::channel::oneshot::Receiver;
use polkadot_node_primitives::approval::VrfSignature;
use polkadot_node_primitives::approval::v2::VrfSignature;
use polkadot_node_subsystem::messages::{AllMessages, BlockDescription};
use polkadot_node_subsystem_test_helpers as test_helpers;
use polkadot_node_subsystem_util::TimeoutExt;
+1
View File
@@ -25,3 +25,4 @@ smallvec = "1.8.0"
substrate-prometheus-endpoint = { path = "../../../substrate/utils/prometheus" }
thiserror = "1.0.48"
async-trait = "0.1.57"
bitvec = { version = "1.0.0", default-features = false, features = ["alloc"] }
@@ -32,7 +32,10 @@ use polkadot_node_network_protocol::{
self as net_protocol, peer_set::PeerSet, request_response::Requests, PeerId,
};
use polkadot_node_primitives::{
approval::{BlockApprovalMeta, IndirectAssignmentCert, IndirectSignedApprovalVote},
approval::{
v1::{BlockApprovalMeta, IndirectSignedApprovalVote},
v2::{CandidateBitfield, IndirectAssignmentCertV2},
},
AvailableData, BabeEpoch, BlockWeight, CandidateVotes, CollationGenerationConfig,
CollationSecondedSignal, DisputeMessage, DisputeStatus, ErasureChunk, PoV,
SignedDisputeStatement, SignedFullStatement, SignedFullStatementWithPVD, SubmitCollationParams,
@@ -841,6 +844,8 @@ pub enum AssignmentCheckError {
InvalidCert(ValidatorIndex, String),
#[error("Internal state mismatch: {0:?}, {1:?}")]
Internal(Hash, CandidateHash),
#[error("Oversized candidate or core bitfield >= {0}")]
InvalidBitfield(usize),
}
/// The result type of [`ApprovalVotingMessage::CheckAndImportApproval`] request.
@@ -906,8 +911,8 @@ pub enum ApprovalVotingMessage {
/// Check if the assignment is valid and can be accepted by our view of the protocol.
/// Should not be sent unless the block hash is known.
CheckAndImportAssignment(
IndirectAssignmentCert,
CandidateIndex,
IndirectAssignmentCertV2,
CandidateBitfield,
oneshot::Sender<AssignmentCheckResult>,
),
/// Check if the approval vote is valid and can be accepted by our view of the
@@ -942,7 +947,7 @@ pub enum ApprovalDistributionMessage {
NewBlocks(Vec<BlockApprovalMeta>),
/// Distribute an assignment cert from the local validator. The cert is assumed
/// to be valid, relevant, and for the given relay-parent and validator index.
DistributeAssignment(IndirectAssignmentCert, CandidateIndex),
DistributeAssignment(IndirectAssignmentCertV2, CandidateBitfield),
/// Distribute an approval vote for the local validator. The approval vote is assumed to be
/// valid, relevant, and the corresponding approval already issued.
/// If not, the subsystem is free to drop the message.
@@ -39,3 +39,6 @@ sc-service = { path = "../../../../../substrate/client/service" }
sp-keyring = { path = "../../../../../substrate/primitives/keyring" }
tokio = { version = "1.24.2", features = ["macros"] }
[features]
network-protocol-staging = [ "polkadot-cli/network-protocol-staging" ]
@@ -75,22 +75,27 @@ struct TrancheEntry {
assignments: Vec<(ValidatorIndex, Tick)>,
}
struct OurAssignment {
cert: AssignmentCert,
tranche: DelayTranche,
validator_index: ValidatorIndex,
triggered: bool,
pub struct OurAssignment {
/// Our assignment certificate.
cert: AssignmentCertV2,
/// The tranche for which the assignment refers to.
tranche: DelayTranche,
/// Our validator index for the session in which the candidates were included.
validator_index: ValidatorIndex,
/// Whether the assignment has been triggered already.
triggered: bool,
}
struct ApprovalEntry {
tranches: Vec<TrancheEntry>, // sorted ascending by tranche number.
backing_group: GroupIndex,
our_assignment: Option<OurAssignment>,
our_approval_sig: Option<ValidatorSignature>,
assignments: Bitfield, // n_validators bits
approved: bool,
pub struct ApprovalEntry {
tranches: Vec<TrancheEntry>, // sorted ascending by tranche number.
backing_group: GroupIndex,
our_assignment: Option<OurAssignment>,
our_approval_sig: Option<ValidatorSignature>,
assigned_validators: Bitfield, // `n_validators` bits.
approved: bool,
}
struct CandidateEntry {
candidate: CandidateReceipt,
session: SessionIndex,
@@ -264,19 +269,25 @@ entry. The cert itself contains information necessary to determine the candidate
* Check the assignment cert
* If the cert kind is `RelayVRFModulo`, then the certificate is valid as long as `sample <
session_info.relay_vrf_samples` and the VRF is valid for the validator's key with the input
`block_entry.relay_vrf_story ++ sample.encode()` as described with [the approvals protocol
section](../../protocol-approval.md#assignment-criteria). We set `core_index = vrf.make_bytes().to_u32() %
session_info.n_cores`. If the `BlockEntry` causes inclusion of a candidate at `core_index`, then this is a valid
assignment for the candidate at `core_index` and has delay tranche 0. Otherwise, it can be ignored.
* If the cert kind is `RelayVRFDelay`, then we check if the VRF is valid for the validator's key with the input
`block_entry.relay_vrf_story ++ cert.core_index.encode()` as described in [the approvals protocol
section](../../protocol-approval.md#assignment-criteria). The cert can be ignored if the block did not cause
inclusion of a candidate on that core index. Otherwise, this is a valid assignment for the included candidate. The
delay tranche for the assignment is determined by reducing `(vrf.make_bytes().to_u64() %
(session_info.n_delay_tranches +
session_info.zeroth_delay_tranche_width)).saturating_sub(session_info.zeroth_delay_tranche_width)`.
* We also check that the core index derived by the output is covered by the `VRFProof` by means of an auxiliary
signature.
`block_entry.relay_vrf_story ++ sample.encode()` as described with
[the approvals protocol section](../../protocol-approval.md#assignment-criteria). We set
`core_index = vrf.make_bytes().to_u32() % session_info.n_cores`. If the `BlockEntry` causes
inclusion of a candidate at `core_index`, then this is a valid assignment for the candidate
at `core_index` and has delay tranche 0. Otherwise, it can be ignored.
* If the cert kind is `RelayVRFModuloCompact`, then the certificate is valid as long as the VRF
is valid for the validator's key with the input `block_entry.relay_vrf_story ++ relay_vrf_samples.encode()`
as described with [the approvals protocol section](../../protocol-approval.md#assignment-criteria).
We enforce that all `core_bitfield` indices are included in the set of the core indices sampled from the
VRF Output. The assignment is considered a valid tranche0 assignment for all claimed candidates if all
`core_bitfield` indices match the core indices where the claimed candidates were included at.
* If the cert kind is `RelayVRFDelay`, then we check if the VRF is valid for the validator's key with the
input `block_entry.relay_vrf_story ++ cert.core_index.encode()` as described in [the approvals protocol
section](../../protocol-approval.md#assignment-criteria). The cert can be ignored if the block did not
cause inclusion of a candidate on that core index. Otherwise, this is a valid assignment for the included
candidate. The delay tranche for the assignment is determined by reducing
`(vrf.make_bytes().to_u64() % (session_info.n_delay_tranches + session_info.zeroth_delay_tranche_width)).saturating_sub(session_info.zeroth_delay_tranche_width)`.
* We also check that the core index derived by the output is covered by the `VRFProof` by means of an auxiliary signature.
* If the delay tranche is too far in the future, return `AssignmentCheckResult::TooFarInFuture`.
* Import the assignment.
* Load the candidate in question and access the `approval_entry` for the block hash the cert references.
@@ -189,9 +189,10 @@ Assignment criteria compute actual assignments using stories and the validators'
Assignment criteria output a `Position` consisting of both a `ParaId` to be checked, as well as a precedence
`DelayTranche` for when the assignment becomes valid.
Assignment criteria come in three flavors, `RelayVRFModulo`, `RelayVRFDelay` and `RelayEquivocation`. Among these, both
`RelayVRFModulo` and `RelayVRFDelay` run a VRF whose input is the output of a `RelayVRFStory`, while `RelayEquivocation`
runs a VRF whose input is the output of a `RelayEquivocationStory`.
Assignment criteria come in four flavors, `RelayVRFModuloCompact`, `RelayVRFDelay`, `RelayEquivocation` and the
deprecated `RelayVRFModulo`. Among these, `RelayVRFModulo`, `RelayVRFModuloCompact` and `RelayVRFDelay` run a
VRF whose input is the output of a `RelayVRFStory`, while `RelayEquivocation` runs a VRF whose input is the
output of a `RelayEquivocationStory`.
Among these, we have two distinct VRF output computations:
@@ -203,6 +204,12 @@ sampled availability core in this relay chain block. We choose three samples in
secure and efficient by increasing this to four or five, and reducing the backing checks accordingly. All successful
`RelayVRFModulo` samples are assigned delay tranche zero.
`RelayVRFModuloCompact` runs a single samples whose VRF input is the `RelayVRFStory` and the sample count. Similar
to `RelayVRFModulo` introduces multiple core assignments for tranche zero. It computes the VRF output with
`schnorrkel::vrf::VRFInOut::make_bytes` using the context "A&V Core v2" and samples up to 160 bytes of the output
as an array of `u32`. Then reduces each `u32` modulo the number of availability cores, and outputs up
to `relay_vrf_modulo_samples` availability core indices.
There is no sampling process for `RelayVRFDelay` and `RelayEquivocation`. We instead run them on specific candidates
and they compute a delay from their VRF output. `RelayVRFDelay` runs for all candidates included under, aka declared
available by, a relay chain block, and inputs the associated VRF output via `RelayVRFStory`. `RelayEquivocation` runs
@@ -223,14 +230,14 @@ We track all validators' announced approval assignments for each candidate assoc
tells us which validators were assigned to which candidates.
We permit at most one assignment per candidate per story per validator, so one validator could be assigned under both
the `RelayVRFDelay` and `RelayEquivocation` criteria, but not under both `RelayVRFModulo` and `RelayVRFDelay` criteria,
since those both use the same story. We permit only one approval vote per candidate per validator, which counts for any
applicable criteria.
the `RelayVRFDelay` and `RelayEquivocation` criteria, but not under both `RelayVRFModulo/RelayVRFModuloCompact`
and `RelayVRFDelay` criteria, since those both use the same story. We permit only one approval vote per candidate per
validator, which counts for any applicable criteria.
We announce, and start checking for, our own assignments when the delay of their tranche is reached, but only if the
tracker says the assignee candidate requires more approval checkers. We never announce an assignment we believe
unnecessary because early announcements gives an adversary information. All delay tranche zero assignments always get
announced, which includes all `RelayVRFModulo` assignments.
tracker says the assignee candidate requires more approval checkers. We never announce an assignment we believe unnecessary
because early announcements gives an adversary information. All delay tranche zero assignments always get announced,
which includes all `RelayVRFModulo` and `RelayVRFModuloCompact` assignments.
In other words, if some candidate `C` needs more approval checkers by the time we reach round `t` then any validators
with an assignment to `C` in delay tranche `t` gossip their send assignment notice for `C`, and begin reconstruction and
@@ -318,10 +325,10 @@ finality. We might explore limits on postponement too, but this sounds much har
## Parameters
We prefer doing approval checkers assignments under `RelayVRFModulo` as opposed to `RelayVRFDelay` because
`RelayVRFModulo` avoids giving individual checkers too many assignments and tranche zero assignments benefit security
the most. We suggest assigning at least 16 checkers under `RelayVRFModulo` although assignment levels have never been
properly analyzed.
We prefer doing approval checkers assignments under `RelayVRFModulo` or `RelayVRFModuloCompact` as opposed to
`RelayVRFDelay` because `RelayVRFModulo` avoids giving individual checkers too many assignments and tranche zero
assignments benefit security the most. We suggest assigning at least 16 checkers under `RelayVRFModulo` or
`RelayVRFModuloCompact` although assignment levels have never been properly analyzed.
Our delay criteria `RelayVRFDelay` and `RelayEquivocation` both have two primary paramaters, expected checkers per
tranche and the zeroth delay tranche width.
@@ -22,6 +22,35 @@ enum AssignmentCertKind {
}
}
enum AssignmentCertKindV2 {
/// Multiple assignment stories based on the VRF that authorized the relay-chain block where the
/// candidates were included.
///
/// The context is [`v2::RELAY_VRF_MODULO_CONTEXT`]
RelayVRFModuloCompact {
/// A bitfield representing the core indices claimed by this assignment.
core_bitfield: CoreBitfield,
},
/// An assignment story based on the VRF that authorized the relay-chain block where the
/// candidate was included combined with the index of a particular core.
///
/// The context is [`v2::RELAY_VRF_DELAY_CONTEXT`]
RelayVRFDelay {
/// The core index chosen in this cert.
core_index: CoreIndex,
},
/// Deprectated assignment. Soon to be removed.
///
/// An assignment story based on the VRF that authorized the relay-chain block where the
/// candidate was included combined with a sample number.
///
/// The context used to produce bytes is [`v1::RELAY_VRF_MODULO_CONTEXT`]
RelayVRFModulo {
/// The sample number used in this cert.
sample: u32,
},
}
struct AssignmentCert {
// The criterion which is claimed to be met by this cert.
kind: AssignmentCertKind,
@@ -55,7 +55,7 @@ struct HostConfiguration {
pub zeroth_delay_tranche_width: u32,
/// The number of validators needed to approve a block.
pub needed_approvals: u32,
/// The number of samples to do of the RelayVRFModulo approval assignment criterion.
/// The number of samples to use in `RelayVRFModulo` or `RelayVRFModuloCompact` approval assignment criterions.
pub relay_vrf_modulo_samples: u32,
/// Total number of individual messages allowed in the parachain -> relay-chain message queue.
pub max_upward_queue_count: u32,
@@ -0,0 +1,39 @@
[settings]
timeout = 1000
bootnode = true
[relaychain.genesis.runtime.configuration.config]
max_validators_per_core = 1
needed_approvals = 7
relay_vrf_modulo_samples = 5
[relaychain]
default_image = "{{ZOMBIENET_INTEGRATION_TEST_IMAGE}}"
chain = "rococo-local"
default_command = "polkadot"
[relaychain.default_resources]
limits = { memory = "4G", cpu = "2" }
requests = { memory = "2G", cpu = "1" }
[[relaychain.node_groups]]
name = "some-validator"
count = 8
args = ["-lparachain=debug,runtime=debug"]
{% for id in range(2000,2005) %}
[[parachains]]
id = {{id}}
addToGenesis = true
genesis_state_generator = "undying-collator export-genesis-state --pov-size={{10000*(id-1999)}} --pvf-complexity={{id - 1999}}"
[parachains.collator]
image = "{{COL_IMAGE}}"
name = "collator"
command = "undying-collator"
args = ["-lparachain=debug", "--pov-size={{10000*(id-1999)}}", "--parachain-id={{id}}", "--pvf-complexity={{id - 1999}}"]
{% endfor %}
[types.Header]
number = "u64"
parent_hash = "Hash"
post_state = "Hash"
@@ -0,0 +1,27 @@
Description: Test if parachains make progress with most of approvals being tranch0
Network: ./0006-parachains-max-tranche0.toml
Creds: config
# Check authority status.
some-validator-0: reports node_roles is 4
some-validator-1: reports node_roles is 4
some-validator-3: reports node_roles is 4
some-validator-4: reports node_roles is 4
some-validator-5: reports node_roles is 4
some-validator-6: reports node_roles is 4
some-validator-7: reports node_roles is 4
some-validator-0: parachain 2000 block height is at least 5 within 180 seconds
some-validator-1: parachain 2001 block height is at least 5 within 180 seconds
some-validator-2: parachain 2002 block height is at least 5 within 180 seconds
some-validator-3: parachain 2003 block height is at least 5 within 180 seconds
some-validator-4: parachain 2004 block height is at least 5 within 180 seconds
some-validator-0: reports polkadot_parachain_approval_checking_finality_lag is lower than 2
some-validator-1: reports polkadot_parachain_approval_checking_finality_lag is lower than 2
some-validator-2: reports polkadot_parachain_approval_checking_finality_lag is lower than 2
some-validator-3: reports polkadot_parachain_approval_checking_finality_lag is lower than 2
some-validator-4: reports polkadot_parachain_approval_checking_finality_lag is lower than 2
some-validator-5: reports polkadot_parachain_approval_checking_finality_lag is lower than 2
some-validator-6: reports polkadot_parachain_approval_checking_finality_lag is lower than 2
some-validator-7: reports polkadot_parachain_approval_checking_finality_lag is lower than 2
+23
View File
@@ -0,0 +1,23 @@
title: tranche0 assignments in one certificate part1
doc:
- audience: Node Operator
description: |
Changed approval-voting, approval-distribution to send all messages tranche0 assignments in one message.
This required:
* A new parachains_db version.
* A new validation protocol to support the new message types.
The new logic will be disabled and will be enabled at a later date after all validators have upgraded.
migrations:
db:
- name: Parachains database change from v3 to v4.
description: |
Approval-voting column format has been updated with several new fields. All existing data will be automatically
be migrated to the new values.
crates:
- name: "polkadot"
semver: patch
host_functions: []