Skip to content
This repository has been archived by the owner on Feb 21, 2024. It is now read-only.

Commit

Permalink
Merge pull request paritytech#296 from subspace/feat/finality_verifier
Browse files Browse the repository at this point in the history
  • Loading branch information
vedhavyas authored Mar 29, 2022
2 parents 46420f7 + 2f85382 commit c451f2b
Show file tree
Hide file tree
Showing 8 changed files with 864 additions and 0 deletions.
22 changes: 22 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions crates/pallet-grandpa-finality-verifier/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[package]
name = "pallet-grandpa-finality-verifier"
version = "1.0.0"
authors = ["Vedhavyas Singareddi <ved@subspace.network>"]
edition = "2021"
license = "Apache-2.0"
homepage = "https://subspace.network"
repository = "https://github.com/subspace/subspace"
description = "Pallet to verify GRANDPA finality proofs for Substrate based chains"
readme = "README.md"

[dependencies]
codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false }
finality-grandpa = { version = "0.15.0", default-features = false }
log = { version = "0.4.14", default-features = false }
num-traits = { version = "0.2", default-features = false }
scale-info = { version = "2.0.1", default-features = false, features = ["derive"] }
serde = { version = "1.0", optional = true }

# Substrate Dependencies

frame-support = { git = "https://github.com/paritytech/substrate", rev="c364008a6c7da8456e17967f55edf51e45146998", default-features = false }
frame-system = { git = "https://github.com/paritytech/substrate", rev="c364008a6c7da8456e17967f55edf51e45146998", default-features = false }
sp-finality-grandpa = { git = "https://github.com/paritytech/substrate", rev="c364008a6c7da8456e17967f55edf51e45146998", default-features = false }
sp-runtime = { git = "https://github.com/paritytech/substrate", rev="c364008a6c7da8456e17967f55edf51e45146998", default-features = false }
sp-std = { git = "https://github.com/paritytech/substrate", rev="c364008a6c7da8456e17967f55edf51e45146998", default-features = false }
sp-trie = { git = "https://github.com/paritytech/substrate", rev="c364008a6c7da8456e17967f55edf51e45146998", default-features = false }

[dev-dependencies]
sp-core = { git = "https://github.com/paritytech/substrate", rev="c364008a6c7da8456e17967f55edf51e45146998" }
sp-io = { git = "https://github.com/paritytech/substrate", rev="c364008a6c7da8456e17967f55edf51e45146998" }
sp-application-crypto = { git = "https://github.com/paritytech/substrate", rev="c364008a6c7da8456e17967f55edf51e45146998" }
ed25519-dalek = { version = "1.0", default-features = false, features = ["u64_backend"] }

[features]
default = ["std"]
std = [
"codec/std",
"finality-grandpa/std",
"frame-support/std",
"frame-system/std",
"log/std",
"num-traits/std",
"scale-info/std",
"serde",
"sp-finality-grandpa/std",
"sp-runtime/std",
"sp-std/std",
"sp-trie/std",
]
5 changes: 5 additions & 0 deletions crates/pallet-grandpa-finality-verifier/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# pallet-finality-verifier

This is a fork of grandpa bridge for finality verification with support for verifying multiple substrate based chains.

License: Apache-2.0
285 changes: 285 additions & 0 deletions crates/pallet-grandpa-finality-verifier/src/grandpa.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
// Copyright (C) 2022 Subspace Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// GRANDPA verification is mostly taken from Parity's bridges https://github.com/paritytech/parity-bridges-common/tree/master/primitives/header-chain
use codec::{Decode, Encode};
use finality_grandpa::voter_set::VoterSet;
use frame_support::Parameter;
use frame_support::RuntimeDebug;
use num_traits::AsPrimitive;
use scale_info::TypeInfo;
#[cfg(feature = "std")]
use serde::{Deserialize, Serialize};
use sp_finality_grandpa::{AuthorityId, AuthorityList, AuthoritySignature, SetId};
use sp_runtime::traits::{
AtLeast32BitUnsigned, Hash as HashT, Header as HeaderT, MaybeDisplay, MaybeMallocSizeOf,
MaybeSerializeDeserialize, Member, Saturating, SimpleBitOps, Verify,
};
use sp_std::collections::{btree_map::BTreeMap, btree_set::BTreeSet};
use sp_std::prelude::*;
use sp_std::{hash::Hash, str::FromStr, vec::Vec};

/// Minimal Substrate-based chain representation that may be used from no_std environment.
pub trait Chain: Send + Sync + 'static {
/// A type that fulfills the abstract idea of what a Substrate block number is.
// Constraits come from the associated Number type of `sp_runtime::traits::Header`
// See here for more info:
// https://crates.parity.io/sp_runtime/traits/trait.Header.html#associatedtype.Number
//
// Note that the `AsPrimitive<usize>` trait is required by the GRANDPA justification
// verifier, and is not usually part of a Substrate Header's Number type.
type BlockNumber: Parameter
+ Member
+ MaybeSerializeDeserialize
+ Hash
+ Copy
+ Default
+ MaybeDisplay
+ AtLeast32BitUnsigned
+ FromStr
+ MaybeMallocSizeOf
+ AsPrimitive<usize>
+ Default
+ Saturating
// original `sp_runtime::traits::Header::BlockNumber` doesn't have this trait, but
// `sp_runtime::generic::Era` requires block number -> `u64` conversion.
+ Into<u64>;

/// A type that fulfills the abstract idea of what a Substrate hash is.
// Constraits come from the associated Hash type of `sp_runtime::traits::Header`
// See here for more info:
// https://crates.parity.io/sp_runtime/traits/trait.Header.html#associatedtype.Hash
type Hash: Parameter
+ Member
+ MaybeSerializeDeserialize
+ Hash
+ Ord
+ Copy
+ MaybeDisplay
+ Default
+ SimpleBitOps
+ AsRef<[u8]>
+ AsMut<[u8]>
+ MaybeMallocSizeOf;

/// A type that fulfills the abstract idea of what a Substrate hasher (a type
/// that produces hashes) is.
// Constraits come from the associated Hashing type of `sp_runtime::traits::Header`
// See here for more info:
// https://crates.parity.io/sp_runtime/traits/trait.Header.html#associatedtype.Hashing
type Hasher: HashT<Output = Self::Hash>;

/// A type that fulfills the abstract idea of what a Substrate header is.
// See here for more info:
// https://crates.parity.io/sp_runtime/traits/trait.Header.html
type Header: Parameter
+ HeaderT<Number = Self::BlockNumber, Hash = Self::Hash>
+ MaybeSerializeDeserialize;

/// Signature type, used on this chain.
type Signature: Parameter + Verify;
}

/// A GRANDPA Justification is a proof that a given header was finalized
/// at a certain height and with a certain set of authorities.
///
/// This particular proof is used to prove that headers on a bridged chain
/// (so not our chain) have been finalized correctly.
#[derive(Encode, Decode, RuntimeDebug, Clone, PartialEq, Eq, TypeInfo)]
pub struct GrandpaJustification<Header: HeaderT> {
/// The round (voting period) this justification is valid for.
pub round: u64,
/// The set of votes for the chain which is to be finalized.
pub commit:
finality_grandpa::Commit<Header::Hash, Header::Number, AuthoritySignature, AuthorityId>,
/// A proof that the chain of blocks in the commit are related to each other.
pub votes_ancestries: Vec<Header>,
}

/// A GRANDPA Authority List and ID.
#[derive(Default, Encode, Decode, RuntimeDebug, PartialEq, Clone, TypeInfo)]
#[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
pub struct AuthoritySet {
/// List of GRANDPA authorities for the current round.
pub authorities: AuthorityList,
/// Monotonic identifier of the current GRANDPA authority set.
pub set_id: SetId,
}

/// Votes ancestries with useful methods.
#[derive(RuntimeDebug)]
struct AncestryChain<Header: HeaderT> {
/// Header hash => parent header hash mapping.
parents: BTreeMap<Header::Hash, Header::Hash>,
/// Hashes of headers that were not visited by `is_ancestor` method.
unvisited: BTreeSet<Header::Hash>,
}

impl<Header: HeaderT> AncestryChain<Header> {
/// Create new ancestry chain.
fn new(ancestry: &[Header]) -> AncestryChain<Header> {
let mut parents = BTreeMap::new();
let mut unvisited = BTreeSet::new();
for ancestor in ancestry {
let hash = ancestor.hash();
let parent_hash = *ancestor.parent_hash();
parents.insert(hash, parent_hash);
unvisited.insert(hash);
}
AncestryChain { parents, unvisited }
}

/// Returns `Ok(_)` if `precommit_target` is a descendant of the `commit_target` block and
/// `Err(_)` otherwise.
fn ensure_descendant(
mut self,
commit_target: &Header::Hash,
precommit_target: &Header::Hash,
) -> Result<Self, Error> {
let mut current_hash = *precommit_target;
while current_hash != *commit_target {
let is_visited_before = !self.unvisited.remove(&current_hash);
current_hash = match self.parents.get(&current_hash) {
Some(parent_hash) => {
if is_visited_before {
// `Some(parent_hash)` means that the `current_hash` is in the `parents`
// container `is_visited_before` means that it has been visited before in
// some of previous calls => since we assume that previous call has finished
// with `true`, this also will be finished with `true`
return Ok(self);
}

*parent_hash
}
None => return Err(Error::PrecommitIsNotCommitDescendant),
};
}

Ok(self)
}
}

/// Justification verification error.
#[derive(RuntimeDebug, PartialEq)]
pub enum Error {
/// Justification is finalizing unexpected header.
InvalidJustificationTarget,
/// The authority has provided an invalid signature.
InvalidAuthoritySignature,
/// The justification contains precommit for header that is not a descendant of the commit
/// header.
PrecommitIsNotCommitDescendant,
/// The cumulative weight of all votes in the justification is not enough to justify commit
/// header finalization.
TooLowCumulativeWeight,
/// The justification contains extra (unused) headers in its `votes_ancestries` field.
ExtraHeadersInVotesAncestries,
}

/// Verify that justification, that is generated by given authority set, finalizes given header.
pub fn verify_justification<Header: HeaderT>(
finalized_target: (Header::Hash, Header::Number),
authorities_set_id: SetId,
authorities_set: &VoterSet<AuthorityId>,
justification: &GrandpaJustification<Header>,
) -> Result<(), Error>
where
Header::Number: finality_grandpa::BlockNumberOps,
{
// ensure that it is justification for the expected header
if (
justification.commit.target_hash,
justification.commit.target_number,
) != finalized_target
{
return Err(Error::InvalidJustificationTarget);
}

let mut chain = AncestryChain::new(&justification.votes_ancestries);
let mut signature_buffer = Vec::new();
let mut votes = BTreeSet::new();
let mut cumulative_weight = 0u64;
for signed in &justification.commit.precommits {
// authority must be in the set
let authority_info = match authorities_set.get(&signed.id) {
Some(authority_info) => authority_info,
None => {
// just ignore precommit from unknown authority as
// `finality_grandpa::import_precommit` does
continue;
}
};

// check if authority has already voted in the same round.
//
// there's a lot of code in `validate_commit` and `import_precommit` functions inside
// `finality-grandpa` crate (mostly related to reporting equivocations). But the only thing
// that we care about is that only first vote from the authority is accepted
if !votes.insert(signed.id.clone()) {
continue;
}

// everything below this line can't just `continue`, because state is already altered

// precommits aren't allowed for block lower than the target
if signed.precommit.target_number < justification.commit.target_number {
return Err(Error::PrecommitIsNotCommitDescendant);
}
// all precommits must be descendants of target block
chain = chain.ensure_descendant(
&justification.commit.target_hash,
&signed.precommit.target_hash,
)?;
// since we know now that the precommit target is the descendant of the justification
// target, we may increase 'weight' of the justification target
//
// there's a lot of code in the `VoteGraph::insert` method inside `finality-grandpa` crate,
// but in the end it is only used to find GHOST, which we don't care about. The only thing
// that we care about is that the justification target has enough weight
cumulative_weight = cumulative_weight.checked_add(authority_info.weight().0.into()).expect(
"sum of weights of ALL authorities is expected not to overflow - this is guaranteed by\
existence of VoterSet;\
the order of loop conditions guarantees that we can account vote from same authority\
multiple times;\
thus we'll never overflow the u64::MAX;\
qed",
);
// verify authority signature
if !sp_finality_grandpa::check_message_signature_with_buffer(
&finality_grandpa::Message::Precommit(signed.precommit.clone()),
&signed.id,
&signed.signature,
justification.round,
authorities_set_id,
&mut signature_buffer,
) {
return Err(Error::InvalidAuthoritySignature);
}
}

// check that there are no extra headers in the justification
if !chain.unvisited.is_empty() {
return Err(Error::ExtraHeadersInVotesAncestries);
}

// check that the cumulative weight of validators voted for the justification target (or one
// of its descendents) is larger than required threshold.
let threshold = authorities_set.threshold().0.into();
if cumulative_weight >= threshold {
Ok(())
} else {
Err(Error::TooLowCumulativeWeight)
}
}
22 changes: 22 additions & 0 deletions crates/pallet-grandpa-finality-verifier/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (C) 2022 Subspace Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#![cfg_attr(not(feature = "std"), no_std)]

mod grandpa;
pub use grandpa::verify_justification;

#[cfg(test)]
mod tests;
Loading

0 comments on commit c451f2b

Please sign in to comment.