Skip to content

Commit

Permalink
Merge pull request #4 from ComposableFi/david-ingest-mmr-proof
Browse files Browse the repository at this point in the history
Ingest Mmr With Proof
  • Loading branch information
Web3 Philosopher authored Apr 4, 2022
2 parents 032638c + c23d78b commit ac8897d
Show file tree
Hide file tree
Showing 10 changed files with 20,167 additions and 18 deletions.
Binary file added .DS_Store
Binary file not shown.
26 changes: 12 additions & 14 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
name: CI Check
name: Cargo Check

on:
push:
workflow_dispatch:
pull_request:
branches:
- '*'

jobs:
checker:
strategy:
matrix:
go-version: [1.17]
os: [ubuntu-latest]
concurrency:
group: lint-${{ github.ref }}
cancel-in-progress: true
container:
image: paritytech/ci-linux:production
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2

- name: ToolChain Selector
uses: actions-rs/toolchain@v1
with:
toolchain: stable

- name: Install srtool

- name: Rustup show
run: |
cargo install --git https://github.com/chevdor/srtool-cli
cargo install --locked --git https://github.com/chevdor/subwasm --tag v0.16.1
rustup show
- name: Checker
run: |
cargo check --no-default-features --target=wasm32-unknown-unknown
cargo check --no-default-features
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
name: Lint

on:
workflow_dispatch:
push:
pull_request:
branches:
- '*'

jobs:
linters:
name: Linters
Expand Down
40 changes: 39 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,45 @@
name = "beefy-generic-client"
version = "0.1.0"
edition = "2021"
authors = ["Composable developers"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

[dependencies]
sp-std = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16" }
sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16" }
sp-core = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16" }
sp-core-hashing = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16" }
sp-io = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16" }

beefy-mmr = { package = "pallet-beefy-mmr", git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16" }
pallet-mmr = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16" }
pallet-mmr-primitives = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16" }
beefy-primitives = { default-features = false, git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16" }
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false, features = ["derive"] }
rs_merkle = { git = "https://github.com/ComposableFi/rs-merkle", default-features = false, branch = "master" }

[dev-dependencies]
subxt = "0.17.0"
tokio = { version = "1.17.0", features = ["full"] }
hex-literal = "0.3.4"
serde_json = "1.0.74"
jsonrpc-core = "18.0.0"
pallet-mmr-rpc = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.16" }

[features]
default = ["std"]
std = [
"codec/std",
"sp-std/std",
"sp-runtime/std",
"sp-core/std",
"sp-core/std",
"sp-io/std",
"beefy-mmr/std",
"beefy-primitives/std",
"pallet-mmr/std",
"pallet-mmr-primitives/std",
"rs_merkle/std"
]
22 changes: 22 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use sp_std::prelude::*;
#[derive(sp_std::fmt::Debug, PartialEq, Eq)]
pub enum BeefyClientError {
/// Failed to read a value from storage
StorageReadError,
/// Failed to write a value to storage
StorageWriteError,
/// Error decoding some value
DecodingError,
/// Invalid Mmr Update
InvalidMmrUpdate,
/// Incomplete Signature threshold
IncompleteSignatureThreshold,
/// Error recovering public key from signature
InvalidSignature,
/// Some invalid merkle root hash
InvalidRootHash,
/// Some invalid mmr proof
InvalidMmrProof,
/// Invalid authority proof
InvalidAuthorityProof,
}
174 changes: 173 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,178 @@
// limitations under the License.
#![cfg_attr(not(feature = "std"), no_std)]

pub mod beefy_light_client {
pub mod error;
pub mod primitives;
#[cfg(test)]
mod runtime;
#[cfg(test)]
mod tests;
pub mod traits;

use crate::error::BeefyClientError;
use crate::primitives::{
BeefyNextAuthoritySet, KeccakHasher, MmrUpdateProof, SignatureWithAuthorityIndex, HASH_LENGTH,
};
use crate::traits::{AuthoritySet, MmrState, StorageRead, StorageWrite};
use beefy_primitives::known_payload_ids::MMR_ROOT_ID;
use codec::Encode;
use sp_core::{ByteArray, H256};
use sp_core_hashing::keccak_256;
use sp_io::crypto;
use sp_runtime::traits::Convert;

use sp_std::prelude::*;

pub struct BeefyLightClient<Store: StorageRead + StorageWrite> {
store: Store,
}

impl<Store: StorageRead + StorageWrite> BeefyLightClient<Store> {
/// Create a new instance of the light client
pub fn new(store: Store) -> Self {
Self { store }
}

/// Return a reference to the underlying store
pub fn store_ref(&self) -> &Store {
&self.store
}

/// This should verify the signed commitment signatures, and reconstruct the
/// authority merkle root, confirming known authorities signed the [`crate::primitives::Commitment`]
/// then using the mmr proofs, verify the latest mmr leaf,
/// using the latest mmr leaf to rotate its view of the next authorities.
pub fn ingest_mmr_root_with_proof(
&mut self,
mmr_update: MmrUpdateProof,
) -> Result<(), BeefyClientError> {
let authority_set = self.store.authority_set()?;
let current_authority_set = &authority_set.current_authorities;
let next_authority_set = &authority_set.next_authorities;
let signatures_len = mmr_update.signed_commitment.signatures.len();
let validator_set_id = mmr_update.signed_commitment.commitment.validator_set_id;

// If signature threshold is not satisfied, return
if !validate_sigs_against_threshold(current_authority_set, signatures_len)
&& !validate_sigs_against_threshold(next_authority_set, signatures_len)
{
return Err(BeefyClientError::IncompleteSignatureThreshold);
}

if current_authority_set.id != validator_set_id && next_authority_set.id != validator_set_id
{
return Err(BeefyClientError::InvalidMmrUpdate);
}

// Extract root hash from signed commitment and validate it
let mmr_root_vec = {
if let Some(root) = mmr_update
.signed_commitment
.commitment
.payload
.get_raw(&MMR_ROOT_ID)
{
if root.len() == HASH_LENGTH {
root
} else {
return Err(BeefyClientError::InvalidRootHash);
}
} else {
return Err(BeefyClientError::InvalidMmrUpdate);
}
};

let mmr_root_hash = H256::from_slice(&*mmr_root_vec);

// Beefy validators sign the keccak_256 hash of the scale encoded commitment
let encoded_commitment = mmr_update.signed_commitment.commitment.encode();
let commitment_hash = keccak_256(&*encoded_commitment);

let mut authority_indices = Vec::new();
let authority_leaves = mmr_update
.signed_commitment
.signatures
.into_iter()
.map(|SignatureWithAuthorityIndex { index, signature }| {
crypto::secp256k1_ecdsa_recover_compressed(&signature, &commitment_hash)
.map(|public_key_bytes| {
beefy_primitives::crypto::AuthorityId::from_slice(&public_key_bytes).ok()
})
.ok()
.flatten()
.map(|pub_key| {
authority_indices.push(index as usize);
keccak_256(&beefy_mmr::BeefyEcdsaToEthereum::convert(pub_key))
})
.ok_or_else(|| BeefyClientError::InvalidSignature)
})
.collect::<Result<Vec<_>, BeefyClientError>>()?;

let mut authorities_changed = false;

let authorities_merkle_proof =
rs_merkle::MerkleProof::<KeccakHasher>::new(mmr_update.authority_proof);

// Verify mmr_update.authority_proof against store root hash
match validator_set_id {
id if id == current_authority_set.id => {
let root_hash = current_authority_set.root;
if !authorities_merkle_proof.verify(
root_hash.into(),
&authority_indices,
&authority_leaves,
current_authority_set.len as usize,
) {
return Err(BeefyClientError::InvalidAuthorityProof);
}
}
id if id == next_authority_set.id => {
let root_hash = next_authority_set.root;
if !authorities_merkle_proof.verify(
root_hash.into(),
&authority_indices,
&authority_leaves,
next_authority_set.len as usize,
) {
return Err(BeefyClientError::InvalidAuthorityProof);
}
authorities_changed = true;
}
_ => return Err(BeefyClientError::InvalidMmrUpdate),
}

let latest_beefy_height = self.store.mmr_state()?.latest_beefy_height;

if mmr_update.signed_commitment.commitment.block_number <= latest_beefy_height {
return Err(BeefyClientError::InvalidMmrUpdate);
}

// Move on to verify mmr_proof
let node = pallet_mmr_primitives::DataOrHash::Data(mmr_update.latest_mmr_leaf.clone());

pallet_mmr::verify_leaf_proof::<sp_runtime::traits::Keccak256, _>(
mmr_root_hash.into(),
node,
mmr_update.mmr_proof,
)
.map_err(|_| BeefyClientError::InvalidMmrProof)?;

self.store.set_mmr_state(MmrState {
latest_beefy_height: mmr_update.signed_commitment.commitment.block_number,
mmr_root_hash: mmr_root_hash.into(),
})?;

if authorities_changed {
self.store.set_authority_set(AuthoritySet {
current_authorities: next_authority_set.clone(),
next_authorities: mmr_update.latest_mmr_leaf.beefy_next_authority_set,
})?;
}
Ok(())
}
}

fn validate_sigs_against_threshold(set: &BeefyNextAuthoritySet<H256>, sigs_len: usize) -> bool {
let threshold = ((2 * set.len) / 3) + 1;
sigs_len >= threshold as usize
}
40 changes: 40 additions & 0 deletions src/primitives.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
pub use beefy_primitives::mmr::{BeefyNextAuthoritySet, MmrLeaf};
use codec::{Decode, Encode};
use sp_core::H256;
use sp_core_hashing::keccak_256;
use sp_std::prelude::*;

pub const HASH_LENGTH: usize = 32;
pub type TSignature = [u8; 65];
pub type Hash = [u8; 32];

#[derive(Clone, sp_std::fmt::Debug, PartialEq, Eq, Encode, Decode)]
pub struct SignatureWithAuthorityIndex {
pub signature: TSignature,
pub index: u32,
}

#[derive(Clone, sp_std::fmt::Debug, PartialEq, Eq, Encode, Decode)]
pub struct SignedCommitment {
pub commitment: beefy_primitives::Commitment<u32>,
pub signatures: Vec<SignatureWithAuthorityIndex>,
}

#[derive(sp_std::fmt::Debug, Clone, Encode, Decode)]
pub struct MmrUpdateProof {
pub signed_commitment: SignedCommitment,
pub latest_mmr_leaf: MmrLeaf<u32, H256, H256>,
pub mmr_proof: pallet_mmr_primitives::Proof<H256>,
pub authority_proof: Vec<Hash>,
}

#[derive(Clone)]
pub struct KeccakHasher;

impl rs_merkle::Hasher for KeccakHasher {
type Hash = [u8; 32];

fn hash(data: &[u8]) -> [u8; 32] {
keccak_256(data)
}
}
Loading

0 comments on commit ac8897d

Please sign in to comment.