// Copyright 2021 Parity Technologies (UK) Ltd.
// This file is part of Parity Bridges Common.
// Parity Bridges Common 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.
// Parity Bridges Common 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 Parity Bridges Common. If not, see .
//! Substrate Finality Verifier Pallet
//!
//! The goal of this pallet is to provide a safe interface for writing finalized headers to an
//! external pallet which tracks headers and finality proofs. By safe, we mean that only headers
//! whose finality has been verified will be written to the underlying pallet.
//!
//! By verifying the finality of headers before writing them to storage we prevent DoS vectors in
//! which unfinalized headers get written to storage even if they don't have a chance of being
//! finalized in the future (such as in the case where a different fork gets finalized).
//!
//! The underlying pallet used for storage is assumed to be a pallet which tracks headers and
//! GRANDPA authority set changes. This information is used during the verification of GRANDPA
//! finality proofs.
#![cfg_attr(not(feature = "std"), no_std)]
// Runtime-generated enums
#![allow(clippy::large_enum_variant)]
use bp_header_chain::{justification::verify_justification, AncestryChecker, HeaderChain};
use bp_runtime::{Chain, HeaderOf};
use finality_grandpa::voter_set::VoterSet;
use frame_support::{dispatch::DispatchError, ensure};
use frame_system::ensure_signed;
use sp_runtime::traits::Header as HeaderT;
use sp_std::vec::Vec;
#[cfg(test)]
mod mock;
// Re-export in crate namespace for `construct_runtime!`
pub use pallet::*;
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
/// Header of the bridged chain.
pub(crate) type BridgedHeader = HeaderOf<::BridgedChain>;
#[pallet::config]
pub trait Config: frame_system::Config {
/// The chain we are bridging to here.
type BridgedChain: Chain;
/// The pallet which we will use as our underlying storage mechanism.
type HeaderChain: HeaderChain<::Header, DispatchError>;
/// The type of ancestry proof used by the pallet.
///
/// Will be used by the ancestry checker to verify that the header being finalized is
/// related to the best finalized header in storage.
type AncestryProof: Parameter;
/// The type through which we will verify that a given header is related to the last
/// finalized header in our storage pallet.
type AncestryChecker: AncestryChecker<::Header, Self::AncestryProof>;
/// The upper bound on the number of requests allowed by the pallet.
///
/// A request refers to an action which writes a header to storage.
///
/// Once this bound is reached the pallet will not allow any dispatchables to be called
/// until the request count has decreased.
#[pallet::constant]
type MaxRequests: Get;
}
#[pallet::pallet]
pub struct Pallet(PhantomData);
#[pallet::hooks]
impl Hooks> for Pallet {
fn on_initialize(_n: T::BlockNumber) -> frame_support::weights::Weight {
>::mutate(|count| *count = count.saturating_sub(1));
(0_u64)
.saturating_add(T::DbWeight::get().reads(1))
.saturating_add(T::DbWeight::get().writes(1))
}
}
#[pallet::call]
impl Pallet {
/// Verify a target header is finalized according to the given finality proof.
///
/// It will use the underlying storage pallet to fetch information about the current
/// authorities and best finalized header in order to verify that the header is finalized.
///
/// If successful in verification, it will write the target header to the underlying storage
/// pallet.
#[pallet::weight(0)]
pub fn submit_finality_proof(
origin: OriginFor,
finality_target: BridgedHeader,
justification: Vec,
ancestry_proof: T::AncestryProof,
) -> DispatchResultWithPostInfo {
let _ = ensure_signed(origin)?;
ensure!(
Self::request_count() < T::MaxRequests::get(),
>::TooManyRequests
);
frame_support::debug::trace!("Going to try and finalize header {:?}", finality_target);
let authority_set = T::HeaderChain::authority_set();
let voter_set = VoterSet::new(authority_set.authorities).ok_or(>::InvalidAuthoritySet)?;
let set_id = authority_set.set_id;
let (hash, number) = (finality_target.hash(), *finality_target.number());
verify_justification::>((hash, number), set_id, voter_set, &justification).map_err(
|e| {
frame_support::debug::error!("Received invalid justification for {:?}: {:?}", finality_target, e);
>::InvalidJustification
},
)?;
let best_finalized = T::HeaderChain::best_finalized();
frame_support::debug::trace!("Checking ancestry against best finalized header: {:?}", &best_finalized);
ensure!(
T::AncestryChecker::are_ancestors(&best_finalized, &finality_target, &ancestry_proof),
>::InvalidAncestryProof
);
let _ = T::HeaderChain::append_header(finality_target)?;
frame_support::debug::info!("Succesfully imported finalized header with hash {:?}!", hash);
>::mutate(|count| *count += 1);
Ok(().into())
}
}
/// The current number of requests which have written to storage.
///
/// If the `RequestCount` hits `MaxRequests`, no more calls will be allowed to the pallet until
/// the request capacity is increased.
///
/// The `RequestCount` is decreased by one at the beginning of every block. This is to ensure
/// that the pallet can always make progress.
#[pallet::storage]
#[pallet::getter(fn request_count)]
pub(super) type RequestCount = StorageValue<_, u32, ValueQuery>;
#[pallet::error]
pub enum Error {
/// The given justification is invalid for the given header.
InvalidJustification,
/// The given ancestry proof is unable to verify that the child and ancestor headers are
/// related.
InvalidAncestryProof,
/// The authority set from the underlying header chain is invalid.
InvalidAuthoritySet,
/// Failed to write a header to the underlying header chain.
FailedToWriteHeader,
/// There are too many requests for the current window to handle.
TooManyRequests,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::{run_test, test_header, Origin, TestRuntime};
use bp_test_utils::{authority_list, make_justification_for_header};
use codec::Encode;
use frame_support::{assert_err, assert_ok};
fn initialize_substrate_bridge() {
let genesis = test_header(0);
let init_data = pallet_substrate_bridge::InitializationData {
header: genesis,
authority_list: authority_list(),
set_id: 1,
scheduled_change: None,
is_halted: false,
};
assert_ok!(pallet_substrate_bridge::Module::::initialize(
Origin::root(),
init_data
));
}
fn submit_finality_proof(child: u8, header: u8) -> frame_support::dispatch::DispatchResultWithPostInfo {
let child = test_header(child.into());
let header = test_header(header.into());
let set_id = 1;
let grandpa_round = 1;
let justification = make_justification_for_header(&header, grandpa_round, set_id, &authority_list()).encode();
let ancestry_proof = vec![child, header.clone()];
Module::::submit_finality_proof(Origin::signed(1), header, justification, ancestry_proof)
}
fn next_block() {
use frame_support::traits::OnInitialize;
let current_number = frame_system::Module::::block_number();
frame_system::Module::::set_block_number(current_number + 1);
let _ = Module::::on_initialize(current_number);
}
#[test]
fn succesfully_imports_header_with_valid_finality_and_ancestry_proofs() {
run_test(|| {
initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2));
let header = test_header(2);
assert_eq!(
pallet_substrate_bridge::Module::::best_headers(),
vec![(*header.number(), header.hash())]
);
assert_eq!(pallet_substrate_bridge::Module::::best_finalized(), header);
})
}
#[test]
fn rejects_justification_that_skips_authority_set_transition() {
run_test(|| {
initialize_substrate_bridge();
let child = test_header(1);
let header = test_header(2);
let set_id = 2;
let grandpa_round = 1;
let justification =
make_justification_for_header(&header, grandpa_round, set_id, &authority_list()).encode();
let ancestry_proof = vec![child, header.clone()];
assert_err!(
Module::::submit_finality_proof(Origin::signed(1), header, justification, ancestry_proof,),
>::InvalidJustification
);
})
}
#[test]
fn does_not_import_header_with_invalid_finality_proof() {
run_test(|| {
initialize_substrate_bridge();
let child = test_header(1);
let header = test_header(2);
let justification = [1u8; 32].encode();
let ancestry_proof = vec![child, header.clone()];
assert_err!(
Module::::submit_finality_proof(Origin::signed(1), header, justification, ancestry_proof,),
>::InvalidJustification
);
})
}
#[test]
fn does_not_import_header_with_invalid_ancestry_proof() {
run_test(|| {
initialize_substrate_bridge();
let header = test_header(2);
let set_id = 1;
let grandpa_round = 1;
let justification =
make_justification_for_header(&header, grandpa_round, set_id, &authority_list()).encode();
// For testing, we've made it so that an empty ancestry proof is invalid
let ancestry_proof = vec![];
assert_err!(
Module::::submit_finality_proof(Origin::signed(1), header, justification, ancestry_proof,),
>::InvalidAncestryProof
);
})
}
#[test]
fn disallows_invalid_authority_set() {
run_test(|| {
use bp_test_utils::{alice, bob};
let genesis = test_header(0);
let invalid_authority_list = vec![(alice(), u64::MAX), (bob(), u64::MAX)];
let init_data = pallet_substrate_bridge::InitializationData {
header: genesis,
authority_list: invalid_authority_list,
set_id: 1,
scheduled_change: None,
is_halted: false,
};
assert_ok!(pallet_substrate_bridge::Module::::initialize(
Origin::root(),
init_data
));
let header = test_header(1);
let justification = [1u8; 32].encode();
let ancestry_proof = vec![];
assert_err!(
Module::::submit_finality_proof(Origin::signed(1), header, justification, ancestry_proof,),
>::InvalidAuthoritySet
);
})
}
#[test]
fn disallows_imports_once_limit_is_hit_in_single_block() {
run_test(|| {
initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2));
assert_ok!(submit_finality_proof(3, 4));
assert_err!(submit_finality_proof(5, 6), >::TooManyRequests);
})
}
#[test]
fn invalid_requests_do_not_count_towards_request_count() {
run_test(|| {
let submit_invalid_request = || {
let child = test_header(1);
let header = test_header(2);
let invalid_justification = vec![4, 2, 4, 2].encode();
let ancestry_proof = vec![child, header.clone()];
Module::::submit_finality_proof(
Origin::signed(1),
header,
invalid_justification,
ancestry_proof,
)
};
initialize_substrate_bridge();
for _ in 0..::MaxRequests::get() + 1 {
// Notice that the error here *isn't* `TooManyRequests`
assert_err!(submit_invalid_request(), >::InvalidJustification);
}
// Can still submit `MaxRequests` requests afterwards
assert_ok!(submit_finality_proof());
assert_ok!(submit_finality_proof());
assert_err!(submit_finality_proof(), >::TooManyRequests);
})
}
#[test]
fn allows_request_after_new_block_has_started() {
run_test(|| {
initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2));
assert_ok!(submit_finality_proof(3, 4));
next_block();
assert_ok!(submit_finality_proof(5, 6));
})
}
#[test]
fn disallows_imports_once_limit_is_hit_across_different_blocks() {
run_test(|| {
initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2));
assert_ok!(submit_finality_proof(3, 4));
next_block();
assert_ok!(submit_finality_proof(5, 6));
assert_err!(submit_finality_proof(7, 8), >::TooManyRequests);
})
}
#[test]
fn allows_max_requests_after_long_time_with_no_activity() {
run_test(|| {
initialize_substrate_bridge();
assert_ok!(submit_finality_proof(1, 2));
assert_ok!(submit_finality_proof(3, 4));
next_block();
next_block();
next_block();
assert_ok!(submit_finality_proof(5, 6));
assert_ok!(submit_finality_proof(7, 8));
})
}
}