Storing multiple Justifications per block (#7640)

* primitives/runtime: initial changes on supporting multiple Justifications

* primitives/runtime: make Justifications strongly typed

* Encode/decode Justifications

* primitives/runtime: add Justification type

* backend: apply_finality and finalize_block takes a single Justification

* manual-seal: create engine id and let rpc take encoded justification

* backend: skeleton functions for appending justifications

* backend: initial implementation append_justification

Initial implementation of append_justification on the Backend trait, and also remove unused skeleton
functions for append_justificaton on Finaziler trait.
k

* backend: guard against duplicate consensus engine id

* client/db: add check for block finality

* client/api: add append_justification to in_mem db

* client/light: add no-op append_justification

* network: fix decode call for Justification

* network: only send a single Justification in BlockData

* network: minor comment update

* protocol: update field names to distinguish single justification

* client: further field renames to plural

* client: update function names to plural justifications

* client/db: upgrade existing database for new format

* network: remove dependency on grandpa crate

* db: fix check for finalized block

* grandpa: check for multiple grandpa justifications hwne importing

* backend: update Finalizer trait to take multiple Justifications

* db: remove debugging statements in migration code

* manual-seal: update note about engine id

* db: fix check for finalized block

* client: update variable name to reflect it is now plural

* grandpa: fix incorrect empty Justications in test

* primitives: make Justifications opaque to avoid being empty

* network: fix detecting empty Justification

* runtime: doc strings for Justifications functions

* runtime: add into_justifications

* primitives: check for duplicates in when adding to Justifications

* network/test: use real grandpa engine id in test

* client: fix reviewer comments

* primitives: rename Justifications::push to append

* backend: revert changes to Finalizer trait

* backend: revert mark_finalized

* backend: revert changes to finalize_block

* backend: revert finalized_blocks

* db: add a quick early return for performance

* client: minor reviewer comments

* service/test: use local ConsensusEngineId

* network: add link to issue for sending multiple Justifications

* Apply suggestions from code review

Co-authored-by: Pierre Krieger <pierre.krieger1708@gmail.com>

* Apply suggestions from code review

Co-authored-by: Pierre Krieger <pierre.krieger1708@gmail.com>

* network: tweaks to review suggestions

* network: revert change to BlockData for backwards compatibility

* Apply suggestion from code review

Co-authored-by: Pierre Krieger <pierre.krieger1708@gmail.com>

* Apply suggestions from code review

Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>

* primitives: update doc comment for Justifications

* client/db/upgrade: avoid grandpa crate dependency

* consensus: revert to single Justification for import_justification

* primitives: improve justifications docs

* style cleanups

* use and_then

* client: rename JUSTIFICATIONS db column

* network: revert to using FRNK in network-test

Co-authored-by: Pierre Krieger <pierre.krieger1708@gmail.com>
Co-authored-by: André Silva <123550+andresilva@users.noreply.github.com>
Co-authored-by: André Silva <andrerfosilva@gmail.com>
This commit is contained in:
Jon Häggblad
2021-03-17 22:18:16 +01:00
committed by GitHub
parent c7d32ba9a6
commit 0d6884b919
43 changed files with 635 additions and 270 deletions
+2 -2
View File
@@ -37,7 +37,7 @@ use libp2p::swarm::{
use log::debug;
use prost::Message;
use sp_consensus::{BlockOrigin, import_queue::{IncomingBlock, Origin}};
use sp_runtime::{traits::{Block as BlockT, NumberFor}, Justification};
use sp_runtime::{traits::{Block as BlockT, NumberFor}, Justifications};
use std::{
borrow::Cow,
collections::{HashSet, VecDeque},
@@ -84,7 +84,7 @@ pub struct Behaviour<B: BlockT> {
/// Event generated by `Behaviour`.
pub enum BehaviourOut<B: BlockT> {
BlockImport(BlockOrigin, Vec<IncomingBlock<B>>),
JustificationImport(Origin, B::Hash, NumberFor<B>, Justification),
JustificationImport(Origin, B::Hash, NumberFor<B>, Justifications),
/// Started a random iterative Kademlia discovery query.
RandomKademliaStarted(ProtocolId),
@@ -275,12 +275,28 @@ impl<B: BlockT> BlockRequestHandler<B> {
let number = *header.number();
let hash = header.hash();
let parent_hash = *header.parent_hash();
let justification = if get_justification {
self.client.justification(&BlockId::Hash(hash))?
let justifications = if get_justification {
self.client.justifications(&BlockId::Hash(hash))?
} else {
None
};
let is_empty_justification = justification.as_ref().map(|j| j.is_empty()).unwrap_or(false);
// TODO: In a follow up PR tracked by https://github.com/paritytech/substrate/issues/8172
// we want to send/receive all justifications.
// For now we keep compatibility by selecting precisely the GRANDPA one, and not just
// the first one. When sending we could have just taken the first one, since we don't
// expect there to be any other kind currently, but when receiving we need to add the
// engine ID tag.
// The ID tag is hardcoded here to avoid depending on the GRANDPA crate, and will be
// removed when resolving the above issue.
let justification = justifications.and_then(|just| just.into_justification(*b"FRNK"));
let is_empty_justification = justification
.as_ref()
.map(|j| j.is_empty())
.unwrap_or(false);
let justification = justification.unwrap_or_default();
let body = if get_body {
match self.client.block_body(&BlockId::Hash(hash))? {
@@ -306,7 +322,7 @@ impl<B: BlockT> BlockRequestHandler<B> {
body,
receipt: Vec::new(),
message_queue: Vec::new(),
justification: justification.unwrap_or_default(),
justification,
is_empty_justification,
};
+2 -2
View File
@@ -52,7 +52,7 @@ fn build_test_full_node(network_config: config::NetworkConfiguration)
&mut self,
origin: sp_consensus::BlockOrigin,
header: B::Header,
justification: Option<sp_runtime::Justification>,
justifications: Option<sp_runtime::Justifications>,
body: Option<Vec<B::Extrinsic>>,
) -> Result<
(
@@ -79,7 +79,7 @@ fn build_test_full_node(network_config: config::NetworkConfiguration)
let mut import = sp_consensus::BlockImportParams::new(origin, header);
import.body = body;
import.finalized = self.0;
import.justification = justification;
import.justifications = justifications;
import.fork_choice = Some(sp_consensus::ForkChoiceStrategy::LongestChain);
Ok((import, maybe_keys))
}
+7 -6
View File
@@ -43,9 +43,10 @@ use sp_consensus::{
block_validation::BlockAnnounceValidator,
import_queue::{BlockImportResult, BlockImportError, IncomingBlock, Origin}
};
use sp_runtime::{generic::BlockId, Justification};
use sp_runtime::traits::{
Block as BlockT, Header as HeaderT, NumberFor, Zero, CheckedSub
use sp_runtime::{
Justifications,
generic::BlockId,
traits::{Block as BlockT, Header as HeaderT, NumberFor, Zero, CheckedSub},
};
use sp_arithmetic::traits::SaturatedConversion;
use sync::{ChainSync, SyncState};
@@ -612,8 +613,8 @@ impl<B: BlockT> Protocol<B> {
if request.fields == message::BlockAttributes::JUSTIFICATION {
match self.sync.on_block_justification(peer_id, block_response) {
Ok(sync::OnBlockJustification::Nothing) => CustomMessageOutcome::None,
Ok(sync::OnBlockJustification::Import { peer, hash, number, justification }) =>
CustomMessageOutcome::JustificationImport(peer, hash, number, justification),
Ok(sync::OnBlockJustification::Import { peer, hash, number, justifications }) =>
CustomMessageOutcome::JustificationImport(peer, hash, number, justifications),
Err(sync::BadPeer(id, repu)) => {
self.behaviour.disconnect_peer(&id, HARDCODED_PEERSETS_SYNC);
self.peerset_handle.report_peer(id, repu);
@@ -1134,7 +1135,7 @@ fn prepare_block_request<B: BlockT>(
#[must_use]
pub enum CustomMessageOutcome<B: BlockT> {
BlockImport(BlockOrigin, Vec<IncomingBlock<B>>),
JustificationImport(Origin, B::Hash, NumberFor<B>, Justification),
JustificationImport(Origin, B::Hash, NumberFor<B>, Justifications),
/// Notification protocols have been opened with a remote.
NotificationStreamOpened {
remote: PeerId,
@@ -148,7 +148,7 @@ pub struct RemoteReadResponse {
pub mod generic {
use bitflags::bitflags;
use codec::{Encode, Decode, Input, Output};
use sp_runtime::Justification;
use sp_runtime::EncodedJustification;
use super::{
RemoteReadResponse, Transactions, Direction,
RequestId, BlockAttributes, RemoteCallResponse, ConsensusEngineId,
@@ -233,7 +233,7 @@ pub mod generic {
/// Block message queue if requested.
pub message_queue: Option<Vec<u8>>,
/// Justification if requested.
pub justification: Option<Justification>,
pub justification: Option<EncodedJustification>,
}
/// Identifies starting point of a block sequence.
+22 -8
View File
@@ -44,7 +44,7 @@ use extra_requests::ExtraRequests;
use libp2p::PeerId;
use log::{debug, trace, warn, info, error};
use sp_runtime::{
Justification,
EncodedJustification, Justifications,
generic::BlockId,
traits::{
Block as BlockT, Header as HeaderT, NumberFor, Zero, One, CheckedSub, SaturatedConversion,
@@ -425,7 +425,7 @@ pub enum OnBlockJustification<B: BlockT> {
peer: PeerId,
hash: B::Hash,
number: NumberFor<B>,
justification: Justification
justifications: Justifications
}
}
@@ -823,11 +823,13 @@ impl<B: BlockT> ChainSync<B> {
.drain(self.best_queued_number + One::one())
.into_iter()
.map(|block_data| {
let justifications =
legacy_justification_mapping(block_data.block.justification);
IncomingBlock {
hash: block_data.block.hash,
header: block_data.block.header,
body: block_data.block.body,
justification: block_data.block.justification,
justifications,
origin: block_data.origin,
allow_missing_state: true,
import_existing: false,
@@ -846,7 +848,7 @@ impl<B: BlockT> ChainSync<B> {
hash: b.hash,
header: b.header,
body: b.body,
justification: b.justification,
justifications: legacy_justification_mapping(b.justification),
origin: Some(who.clone()),
allow_missing_state: true,
import_existing: false,
@@ -955,7 +957,7 @@ impl<B: BlockT> ChainSync<B> {
hash: b.hash,
header: b.header,
body: b.body,
justification: b.justification,
justifications: legacy_justification_mapping(b.justification),
origin: Some(who.clone()),
allow_missing_state: true,
import_existing: false,
@@ -1039,8 +1041,11 @@ impl<B: BlockT> ChainSync<B> {
None
};
if let Some((peer, hash, number, j)) = self.extra_justifications.on_response(who, justification) {
return Ok(OnBlockJustification::Import { peer, hash, number, justification: j })
if let Some((peer, hash, number, j)) = self
.extra_justifications
.on_response(who, legacy_justification_mapping(justification))
{
return Ok(OnBlockJustification::Import { peer, hash, number, justifications: j })
}
}
@@ -1597,6 +1602,14 @@ impl<B: BlockT> ChainSync<B> {
}
}
// This is purely during a backwards compatible transitionary period and should be removed
// once we can assume all nodes can send and receive multiple Justifications
// The ID tag is hardcoded here to avoid depending on the GRANDPA crate.
// TODO: https://github.com/paritytech/substrate/issues/8172
fn legacy_justification_mapping(justification: Option<EncodedJustification>) -> Option<Justifications> {
justification.map(|just| (*b"FRNK", just).into())
}
#[derive(Debug)]
pub(crate) struct Metrics {
pub(crate) queued_blocks: u32,
@@ -2396,7 +2409,8 @@ mod test {
);
let finalized_block = blocks[MAX_BLOCKS_TO_LOOK_BACKWARDS as usize * 2 - 1].clone();
client.finalize_block(BlockId::Hash(finalized_block.hash()), Some(Vec::new())).unwrap();
let just = (*b"TEST", Vec::new());
client.finalize_block(BlockId::Hash(finalized_block.hash()), Some(just)).unwrap();
sync.update_chain_info(&info.best_hash, info.best_number);
let peer_id1 = PeerId::random();
+2 -2
View File
@@ -1452,11 +1452,11 @@ impl<B: BlockT + 'static, H: ExHashT> Future for NetworkWorker<B, H> {
}
this.import_queue.import_blocks(origin, blocks);
},
Poll::Ready(SwarmEvent::Behaviour(BehaviourOut::JustificationImport(origin, hash, nb, justification))) => {
Poll::Ready(SwarmEvent::Behaviour(BehaviourOut::JustificationImport(origin, hash, nb, justifications))) => {
if let Some(metrics) = this.metrics.as_ref() {
metrics.import_queue_justifications_submitted.inc();
}
this.import_queue.import_justification(origin, hash, nb, justification);
this.import_queue.import_justifications(origin, hash, nb, justifications);
},
Poll::Ready(SwarmEvent::Behaviour(BehaviourOut::InboundRequest { protocol, result, .. })) => {
if let Some(metrics) = this.metrics.as_ref() {
@@ -52,7 +52,7 @@ fn build_test_full_node(config: config::NetworkConfiguration)
&mut self,
origin: sp_consensus::BlockOrigin,
header: B::Header,
justification: Option<sp_runtime::Justification>,
justifications: Option<sp_runtime::Justifications>,
body: Option<Vec<B::Extrinsic>>,
) -> Result<
(
@@ -79,7 +79,7 @@ fn build_test_full_node(config: config::NetworkConfiguration)
let mut import = sp_consensus::BlockImportParams::new(origin, header);
import.body = body;
import.finalized = self.0;
import.justification = justification;
import.justifications = justifications;
import.fork_choice = Some(sp_consensus::ForkChoiceStrategy::LongestChain);
Ok((import, maybe_keys))
}
@@ -35,13 +35,13 @@ fn prepare_good_block() -> (TestClient, Hash, u64, PeerId, IncomingBlock<Block>)
let (hash, number) = (client.block_hash(1).unwrap().unwrap(), 1);
let header = client.header(&BlockId::Number(1)).unwrap();
let justification = client.justification(&BlockId::Number(1)).unwrap();
let justifications = client.justifications(&BlockId::Number(1)).unwrap();
let peer_id = PeerId::random();
(client, hash, number, peer_id.clone(), IncomingBlock {
hash,
header,
body: Some(Vec::new()),
justification,
justifications,
origin: Some(peer_id.clone()),
allow_missing_state: false,
import_existing: false,
+8 -8
View File
@@ -63,7 +63,7 @@ use sp_core::H256;
use sc_network::config::ProtocolConfig;
use sp_runtime::generic::{BlockId, OpaqueDigestItemId};
use sp_runtime::traits::{Block as BlockT, Header as HeaderT, NumberFor};
use sp_runtime::Justification;
use sp_runtime::{Justification, Justifications};
use substrate_test_runtime_client::{self, AccountKeyring};
use sc_service::client::Client;
pub use sc_network::config::EmptyTransactionPool;
@@ -109,7 +109,7 @@ impl<B: BlockT> Verifier<B> for PassThroughVerifier {
&mut self,
origin: BlockOrigin,
header: B::Header,
justification: Option<Justification>,
justifications: Option<Justifications>,
body: Option<Vec<B::Extrinsic>>
) -> Result<(BlockImportParams<B, ()>, Option<Vec<(CacheKeyId, Vec<u8>)>>), String> {
let maybe_keys = header.digest()
@@ -120,7 +120,7 @@ impl<B: BlockT> Verifier<B> for PassThroughVerifier {
let mut import = BlockImportParams::new(origin, header);
import.body = body;
import.finalized = self.finalized;
import.justification = justification;
import.justifications = justifications;
import.fork_choice = Some(self.fork_choice.clone());
Ok((import, maybe_keys))
@@ -184,10 +184,10 @@ impl PeersClient {
}
}
pub fn justification(&self, block: &BlockId<Block>) -> ClientResult<Option<Justification>> {
pub fn justifications(&self, block: &BlockId<Block>) -> ClientResult<Option<Justifications>> {
match *self {
PeersClient::Full(ref client, ref _backend) => client.justification(block),
PeersClient::Light(ref client, ref _backend) => client.justification(block),
PeersClient::Full(ref client, ref _backend) => client.justifications(block),
PeersClient::Light(ref client, ref _backend) => client.justifications(block),
}
}
@@ -577,11 +577,11 @@ impl<B: BlockT> Verifier<B> for VerifierAdapter<B> {
&mut self,
origin: BlockOrigin,
header: B::Header,
justification: Option<Justification>,
justifications: Option<Justifications>,
body: Option<Vec<B::Extrinsic>>
) -> Result<(BlockImportParams<B, ()>, Option<Vec<(CacheKeyId, Vec<u8>)>>), String> {
let hash = header.hash();
self.verifier.lock().verify(origin, header, justification, body).map_err(|e| {
self.verifier.lock().verify(origin, header, justifications, body).map_err(|e| {
self.failed_verifications.lock().insert(hash, e.clone());
e
})
+47 -16
View File
@@ -22,6 +22,7 @@ use futures::{Future, executor::block_on};
use super::*;
use sp_consensus::block_validation::Validation;
use substrate_test_runtime::Header;
use sp_runtime::Justifications;
fn test_ancestor_search_when_common_is(n: usize) {
sp_tracing::try_init_simple();
@@ -248,13 +249,14 @@ fn sync_justifications() {
net.block_until_sync();
// there's currently no justification for block #10
assert_eq!(net.peer(0).client().justification(&BlockId::Number(10)).unwrap(), None);
assert_eq!(net.peer(1).client().justification(&BlockId::Number(10)).unwrap(), None);
assert_eq!(net.peer(0).client().justifications(&BlockId::Number(10)).unwrap(), None);
assert_eq!(net.peer(1).client().justifications(&BlockId::Number(10)).unwrap(), None);
// we finalize block #10, #15 and #20 for peer 0 with a justification
net.peer(0).client().finalize_block(BlockId::Number(10), Some(Vec::new()), true).unwrap();
net.peer(0).client().finalize_block(BlockId::Number(15), Some(Vec::new()), true).unwrap();
net.peer(0).client().finalize_block(BlockId::Number(20), Some(Vec::new()), true).unwrap();
let just = (*b"FRNK", Vec::new());
net.peer(0).client().finalize_block(BlockId::Number(10), Some(just.clone()), true).unwrap();
net.peer(0).client().finalize_block(BlockId::Number(15), Some(just.clone()), true).unwrap();
net.peer(0).client().finalize_block(BlockId::Number(20), Some(just.clone()), true).unwrap();
let h1 = net.peer(1).client().header(&BlockId::Number(10)).unwrap().unwrap();
let h2 = net.peer(1).client().header(&BlockId::Number(15)).unwrap().unwrap();
@@ -269,10 +271,20 @@ fn sync_justifications() {
net.poll(cx);
for height in (10..21).step_by(5) {
if net.peer(0).client().justification(&BlockId::Number(height)).unwrap() != Some(Vec::new()) {
if net
.peer(0)
.client()
.justifications(&BlockId::Number(height))
.unwrap() != Some(Justifications::from((*b"FRNK", Vec::new())))
{
return Poll::Pending;
}
if net.peer(1).client().justification(&BlockId::Number(height)).unwrap() != Some(Vec::new()) {
if net
.peer(1)
.client()
.justifications(&BlockId::Number(height))
.unwrap() != Some(Justifications::from((*b"FRNK", Vec::new())))
{
return Poll::Pending;
}
}
@@ -295,7 +307,8 @@ fn sync_justifications_across_forks() {
// for both and finalize the small fork instead.
net.block_until_sync();
net.peer(0).client().finalize_block(BlockId::Hash(f1_best), Some(Vec::new()), true).unwrap();
let just = (*b"FRNK", Vec::new());
net.peer(0).client().finalize_block(BlockId::Hash(f1_best), Some(just), true).unwrap();
net.peer(1).request_justification(&f1_best, 10);
net.peer(1).request_justification(&f2_best, 11);
@@ -303,8 +316,16 @@ fn sync_justifications_across_forks() {
block_on(futures::future::poll_fn::<(), _>(|cx| {
net.poll(cx);
if net.peer(0).client().justification(&BlockId::Number(10)).unwrap() == Some(Vec::new()) &&
net.peer(1).client().justification(&BlockId::Number(10)).unwrap() == Some(Vec::new())
if net
.peer(0)
.client()
.justifications(&BlockId::Number(10))
.unwrap() == Some(Justifications::from((*b"FRNK", Vec::new())))
&& net
.peer(1)
.client()
.justifications(&BlockId::Number(10))
.unwrap() == Some(Justifications::from((*b"FRNK", Vec::new())))
{
Poll::Ready(())
} else {
@@ -696,8 +717,9 @@ fn can_sync_to_peers_with_wrong_common_block() {
net.block_until_connected();
// both peers re-org to the same fork without notifying each other
net.peer(0).client().finalize_block(BlockId::Hash(fork_hash), Some(Vec::new()), true).unwrap();
net.peer(1).client().finalize_block(BlockId::Hash(fork_hash), Some(Vec::new()), true).unwrap();
let just = Some((*b"FRNK", Vec::new()));
net.peer(0).client().finalize_block(BlockId::Hash(fork_hash), just.clone(), true).unwrap();
net.peer(1).client().finalize_block(BlockId::Hash(fork_hash), just, true).unwrap();
let final_hash = net.peer(0).push_blocks(1, false);
net.block_until_sync();
@@ -948,8 +970,8 @@ fn multiple_requests_are_accepted_as_long_as_they_are_not_fulfilled() {
net.block_until_sync();
// there's currently no justification for block #10
assert_eq!(net.peer(0).client().justification(&BlockId::Number(10)).unwrap(), None);
assert_eq!(net.peer(1).client().justification(&BlockId::Number(10)).unwrap(), None);
assert_eq!(net.peer(0).client().justifications(&BlockId::Number(10)).unwrap(), None);
assert_eq!(net.peer(1).client().justifications(&BlockId::Number(10)).unwrap(), None);
let h1 = net.peer(1).client().header(&BlockId::Number(10)).unwrap().unwrap();
@@ -967,12 +989,21 @@ fn multiple_requests_are_accepted_as_long_as_they_are_not_fulfilled() {
}
// Finalize the block and make the justification available.
net.peer(0).client().finalize_block(BlockId::Number(10), Some(Vec::new()), true).unwrap();
net.peer(0).client().finalize_block(
BlockId::Number(10),
Some((*b"FRNK", Vec::new())),
true,
).unwrap();
block_on(futures::future::poll_fn::<(), _>(|cx| {
net.poll(cx);
if net.peer(1).client().justification(&BlockId::Number(10)).unwrap() != Some(Vec::new()) {
if net
.peer(1)
.client()
.justifications(&BlockId::Number(10))
.unwrap() != Some(Justifications::from((*b"FRNK", Vec::new())))
{
return Poll::Pending;
}