-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Merkle Mountain Range pallet improvements #7891
Changes from 9 commits
35e0139
9ba97f9
3398d24
e91de2d
2d30b44
65e4b30
dca5a1d
eaf5405
07fe645
9ccd92b
7cc7e94
63eef7a
4e1f3dc
3199722
080d1d3
f673f22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
[package] | ||
name = "pallet-mmr-primitives" | ||
version = "2.0.0" | ||
authors = ["Parity Technologies <admin@parity.io>"] | ||
edition = "2018" | ||
license = "Apache-2.0" | ||
homepage = "https://substrate.dev" | ||
repository = "https://github.com/paritytech/substrate/" | ||
description = "FRAME Merkle Mountain Range primitives." | ||
|
||
[package.metadata.docs.rs] | ||
targets = ["x86_64-unknown-linux-gnu"] | ||
|
||
[dependencies] | ||
codec = { package = "parity-scale-codec", version = "1.3.6", default-features = false } | ||
frame-support = { version = "2.0.0", default-features = false, path = "../../support" } | ||
frame-system = { version = "2.0.0", default-features = false, path = "../../system" } | ||
serde = { version = "1.0.101", optional = true, features = ["derive"] } | ||
sp-api = { version = "2.0.0", default-features = false, path = "../../../primitives/api" } | ||
sp-core = { version = "2.0.0", default-features = false, path = "../../../primitives/core" } | ||
sp-runtime = { version = "2.0.0", default-features = false, path = "../../../primitives/runtime" } | ||
sp-std = { version = "2.0.0", default-features = false, path = "../../../primitives/std" } | ||
|
||
[dev-dependencies] | ||
hex-literal = "0.3" | ||
|
||
[features] | ||
default = ["std"] | ||
std = [ | ||
"codec/std", | ||
"frame-support/std", | ||
"frame-system/std", | ||
"serde", | ||
"sp-api/std", | ||
"sp-core/std", | ||
"sp-runtime/std", | ||
"sp-std/std", | ||
] |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -17,7 +17,10 @@ | |||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
//! Merkle Mountain Range primitive types. | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
use frame_support::RuntimeDebug; | ||||||||||||||||||||||||||||||||||||||||||||||
#![cfg_attr(not(feature = "std"), no_std)] | ||||||||||||||||||||||||||||||||||||||||||||||
#![warn(missing_docs)] | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
use frame_support::{RuntimeDebug, debug}; | ||||||||||||||||||||||||||||||||||||||||||||||
use sp_runtime::traits; | ||||||||||||||||||||||||||||||||||||||||||||||
use sp_std::fmt; | ||||||||||||||||||||||||||||||||||||||||||||||
#[cfg(not(feature = "std"))] | ||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -127,7 +130,7 @@ mod encoding { | |||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
impl<H: traits::Hash, L: FullLeaf> codec::Decode for DataOrHash<H, L> { | ||||||||||||||||||||||||||||||||||||||||||||||
impl<H: traits::Hash, L: FullLeaf + codec::Decode> codec::Decode for DataOrHash<H, L> { | ||||||||||||||||||||||||||||||||||||||||||||||
fn decode<I: codec::Input>(value: &mut I) -> Result<Self, codec::Error> { | ||||||||||||||||||||||||||||||||||||||||||||||
let decoded: Either<Vec<u8>, H::Output> = Either::decode(value)?; | ||||||||||||||||||||||||||||||||||||||||||||||
Ok(match decoded { | ||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -164,6 +167,7 @@ impl<H: traits::Hash, L: FullLeaf> DataOrHash<H, L> { | |||||||||||||||||||||||||||||||||||||||||||||
/// you don't care about with their hashes. | ||||||||||||||||||||||||||||||||||||||||||||||
#[derive(RuntimeDebug, Clone, PartialEq)] | ||||||||||||||||||||||||||||||||||||||||||||||
pub struct Compact<H, T> { | ||||||||||||||||||||||||||||||||||||||||||||||
/// Internal tuple representation. | ||||||||||||||||||||||||||||||||||||||||||||||
pub tuple: T, | ||||||||||||||||||||||||||||||||||||||||||||||
_hash: sp_std::marker::PhantomData<H>, | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -177,6 +181,7 @@ impl<H, T> sp_std::ops::Deref for Compact<H, T> { | |||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
impl<H, T> Compact<H, T> { | ||||||||||||||||||||||||||||||||||||||||||||||
/// Create a new [Compact] wrapper for a tuple. | ||||||||||||||||||||||||||||||||||||||||||||||
pub fn new(tuple: T) -> Self { | ||||||||||||||||||||||||||||||||||||||||||||||
Self { tuple, _hash: Default::default() } | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -274,15 +279,114 @@ pub struct Proof<Hash> { | |||||||||||||||||||||||||||||||||||||||||||||
pub items: Vec<Hash>, | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
/// Merkle Mountain Range operation error. | ||||||||||||||||||||||||||||||||||||||||||||||
#[derive(RuntimeDebug, codec::Encode, codec::Decode, PartialEq, Eq)] | ||||||||||||||||||||||||||||||||||||||||||||||
pub enum Error { | ||||||||||||||||||||||||||||||||||||||||||||||
/// Error while pushing new node. | ||||||||||||||||||||||||||||||||||||||||||||||
Push, | ||||||||||||||||||||||||||||||||||||||||||||||
/// Error getting the new root. | ||||||||||||||||||||||||||||||||||||||||||||||
GetRoot, | ||||||||||||||||||||||||||||||||||||||||||||||
/// Error commiting changes. | ||||||||||||||||||||||||||||||||||||||||||||||
Commit, | ||||||||||||||||||||||||||||||||||||||||||||||
/// Error during proof generation. | ||||||||||||||||||||||||||||||||||||||||||||||
GenerateProof, | ||||||||||||||||||||||||||||||||||||||||||||||
/// Proof verification error. | ||||||||||||||||||||||||||||||||||||||||||||||
Verify, | ||||||||||||||||||||||||||||||||||||||||||||||
/// Leaf not found in the storage. | ||||||||||||||||||||||||||||||||||||||||||||||
LeafNotFound, | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
impl Error { | ||||||||||||||||||||||||||||||||||||||||||||||
#![allow(unused_variables)] | ||||||||||||||||||||||||||||||||||||||||||||||
HCastano marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||
/// Consume given error `e` with `self` and generate a native log entry with error details. | ||||||||||||||||||||||||||||||||||||||||||||||
pub fn log_error(self, e: impl fmt::Debug) -> Self { | ||||||||||||||||||||||||||||||||||||||||||||||
debug::native::error!("[{:?}] MMR error: {:?}", self, e); | ||||||||||||||||||||||||||||||||||||||||||||||
self | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
/// Consume given error `e` with `self` and generate a native log entry with error details. | ||||||||||||||||||||||||||||||||||||||||||||||
pub fn log_debug(self, e: impl fmt::Debug) -> Self { | ||||||||||||||||||||||||||||||||||||||||||||||
debug::native::debug!("[{:?}] MMR error: {:?}", self, e); | ||||||||||||||||||||||||||||||||||||||||||||||
self | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
/// A helper type to allow using arbitrary SCALE-encoded leaf data in the RuntimeApi. | ||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||
/// The point is to be able to verify MMR proofs from external MMRs, where we don't | ||||||||||||||||||||||||||||||||||||||||||||||
/// know the exact leaf type, but it's enough for us to have it SCALE-encoded. | ||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||
/// Note the leaf type should be encoded in it's compact form when passed through this type. | ||||||||||||||||||||||||||||||||||||||||||||||
tomusdrw marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||
/// See [FullLeaf] documentation for details. | ||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||
/// This type is SCALE-compatible with `Vec<u8>` encoding. I.e. you must encode your leaf twice: | ||||||||||||||||||||||||||||||||||||||||||||||
/// ```rust,ignore | ||||||||||||||||||||||||||||||||||||||||||||||
/// let encoded: Vec<u8> = my_leaf.encode(); | ||||||||||||||||||||||||||||||||||||||||||||||
/// let opaque: Vec<u8> = encoded.encode(); | ||||||||||||||||||||||||||||||||||||||||||||||
/// let decoded = OpaqueLeaf::decode(&mut &*opaque); | ||||||||||||||||||||||||||||||||||||||||||||||
/// ``` | ||||||||||||||||||||||||||||||||||||||||||||||
tomusdrw marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))] | ||||||||||||||||||||||||||||||||||||||||||||||
#[derive(RuntimeDebug, Clone, PartialEq, codec::Decode)] | ||||||||||||||||||||||||||||||||||||||||||||||
pub struct OpaqueLeaf( | ||||||||||||||||||||||||||||||||||||||||||||||
/// Leaf type encoded in it's compact form. | ||||||||||||||||||||||||||||||||||||||||||||||
HCastano marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||
#[cfg_attr(feature = "std", serde(with = "sp_core::bytes"))] | ||||||||||||||||||||||||||||||||||||||||||||||
pub Vec<u8> | ||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
impl OpaqueLeaf { | ||||||||||||||||||||||||||||||||||||||||||||||
/// Convert a concrete MMR leaf into an opaque type. | ||||||||||||||||||||||||||||||||||||||||||||||
pub fn from_leaf<T: FullLeaf>(leaf: T) -> Self { | ||||||||||||||||||||||||||||||||||||||||||||||
let encoded_leaf = leaf.using_encoded(|d| d.to_vec(), true); | ||||||||||||||||||||||||||||||||||||||||||||||
// OpaqueLeaf must be SCALE-compatible with `Vec<u8>`. | ||||||||||||||||||||||||||||||||||||||||||||||
// Simply using raw encoded bytes don't work, cause we don't know the | ||||||||||||||||||||||||||||||||||||||||||||||
// length of the expected data. | ||||||||||||||||||||||||||||||||||||||||||||||
let encoded_vec = codec::Encode::encode(&encoded_leaf); | ||||||||||||||||||||||||||||||||||||||||||||||
OpaqueLeaf(encoded_vec) | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
impl FullLeaf for OpaqueLeaf { | ||||||||||||||||||||||||||||||||||||||||||||||
fn using_encoded<R, F: FnOnce(&[u8]) -> R>(&self, f: F, _compact: bool) -> R { | ||||||||||||||||||||||||||||||||||||||||||||||
f(&self.0) | ||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this type encoding/decoding is bit confusing to me. I think it would make more sense to have a manual implementation of Decode.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was writing an elaborate answer why this is correct, just to learn that it's all wrong :) Thanks for being persistent with this. So my initialy motivation was to have Please see the updated version now. The runtime API just accepts a raw |
||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
sp_api::decl_runtime_apis! { | ||||||||||||||||||||||||||||||||||||||||||||||
/// API to interact with MMR pallet. | ||||||||||||||||||||||||||||||||||||||||||||||
pub trait MmrApi<Leaf: codec::Codec, Hash: codec::Codec> { | ||||||||||||||||||||||||||||||||||||||||||||||
/// Generate MMR proof for a leaf under given index. | ||||||||||||||||||||||||||||||||||||||||||||||
fn generate_proof(leaf_index: u64) -> Result<(Leaf, Proof<Hash>), Error>; | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
/// Verify MMR proof against on-chain MMR. | ||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||
/// Note this function will use on-chain MMR root hash and check if the proof | ||||||||||||||||||||||||||||||||||||||||||||||
/// matches the hash. | ||||||||||||||||||||||||||||||||||||||||||||||
/// See [Self::verify_proof_stateless] for a stateless verifier. | ||||||||||||||||||||||||||||||||||||||||||||||
fn verify_proof(leaf: Leaf, proof: Proof<Hash>) -> Result<(), Error>; | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
/// Verify MMR proof against given root hash. | ||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||
/// Note this function does not require any on-chain storage - the | ||||||||||||||||||||||||||||||||||||||||||||||
/// proof is verified against given MMR root hash. | ||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||
/// The leaf data is expected to be encoded in it's compact form. | ||||||||||||||||||||||||||||||||||||||||||||||
fn verify_proof_stateless(root: Hash, leaf: Vec<u8>, proof: Proof<Hash>) | ||||||||||||||||||||||||||||||||||||||||||||||
-> Result<(), Error>; | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
#[cfg(test)] | ||||||||||||||||||||||||||||||||||||||||||||||
mod tests { | ||||||||||||||||||||||||||||||||||||||||||||||
use super::*; | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
use codec::Decode; | ||||||||||||||||||||||||||||||||||||||||||||||
use crate::tests::hex; | ||||||||||||||||||||||||||||||||||||||||||||||
use sp_core::H256; | ||||||||||||||||||||||||||||||||||||||||||||||
use sp_runtime::traits::Keccak256; | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
pub(crate) fn hex(s: &str) -> H256 { | ||||||||||||||||||||||||||||||||||||||||||||||
s.parse().unwrap() | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
type Test = DataOrHash<Keccak256, String>; | ||||||||||||||||||||||||||||||||||||||||||||||
type TestCompact = Compact<Keccak256, (Test, Test)>; | ||||||||||||||||||||||||||||||||||||||||||||||
type TestProof = Proof<<Keccak256 as traits::Hash>::Output>; | ||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -412,4 +516,57 @@ mod tests { | |||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
assert_eq!(decoded_compact, vec![Ok(d.clone()), Ok(d.clone())]); | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
#[test] | ||||||||||||||||||||||||||||||||||||||||||||||
fn opaque_leaves_should_be_scale_compatible_with_concrete_ones() { | ||||||||||||||||||||||||||||||||||||||||||||||
tomusdrw marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||
// given | ||||||||||||||||||||||||||||||||||||||||||||||
let a = Test::Data("Hello World!".into()); | ||||||||||||||||||||||||||||||||||||||||||||||
let b = Test::Data("".into()); | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
let c: TestCompact = Compact::new((a.clone(), b.clone())); | ||||||||||||||||||||||||||||||||||||||||||||||
let d: TestCompact = Compact::new(( | ||||||||||||||||||||||||||||||||||||||||||||||
Test::Hash(a.hash()), | ||||||||||||||||||||||||||||||||||||||||||||||
Test::Hash(b.hash()), | ||||||||||||||||||||||||||||||||||||||||||||||
)); | ||||||||||||||||||||||||||||||||||||||||||||||
let cases = vec![c, d.clone()]; | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
let encoded_compact = cases | ||||||||||||||||||||||||||||||||||||||||||||||
.iter() | ||||||||||||||||||||||||||||||||||||||||||||||
.map(|c| c.using_encoded(|x| x.to_vec(), true)) | ||||||||||||||||||||||||||||||||||||||||||||||
.collect::<Vec<_>>(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
let decoded_opaque = encoded_compact | ||||||||||||||||||||||||||||||||||||||||||||||
.iter() | ||||||||||||||||||||||||||||||||||||||||||||||
// Encode the Vec<u8> again. | ||||||||||||||||||||||||||||||||||||||||||||||
.map(codec::Encode::encode) | ||||||||||||||||||||||||||||||||||||||||||||||
.map(|x| OpaqueLeaf::decode(&mut &*x)) | ||||||||||||||||||||||||||||||||||||||||||||||
.collect::<Vec<_>>(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
let reencoded = decoded_opaque | ||||||||||||||||||||||||||||||||||||||||||||||
.iter() | ||||||||||||||||||||||||||||||||||||||||||||||
.map(|x| x.as_ref().map(|x| x.using_encoded(|x| x.to_vec(), false))) | ||||||||||||||||||||||||||||||||||||||||||||||
.collect::<Vec<_>>(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
let reencoded_compact = decoded_opaque | ||||||||||||||||||||||||||||||||||||||||||||||
.iter() | ||||||||||||||||||||||||||||||||||||||||||||||
.map(|x| x.as_ref().map(|x| x.using_encoded(|x| x.to_vec(), true))) | ||||||||||||||||||||||||||||||||||||||||||||||
.collect::<Vec<_>>(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
// then | ||||||||||||||||||||||||||||||||||||||||||||||
// make sure that re-encoding opaque leaves end up with the same encoding. | ||||||||||||||||||||||||||||||||||||||||||||||
assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||||
reencoded, | ||||||||||||||||||||||||||||||||||||||||||||||
encoded_compact.clone().into_iter().map(Ok).collect::<Vec<_>>() | ||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||
// make sure that compact and non-compact encoding is the same. | ||||||||||||||||||||||||||||||||||||||||||||||
assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||||
reencoded, | ||||||||||||||||||||||||||||||||||||||||||||||
reencoded_compact | ||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||
// make sure that decoded opaque leaves simply contain raw bytes payload. | ||||||||||||||||||||||||||||||||||||||||||||||
assert_eq!( | ||||||||||||||||||||||||||||||||||||||||||||||
decoded_opaque, | ||||||||||||||||||||||||||||||||||||||||||||||
encoded_compact.into_iter().map(OpaqueLeaf).map(Ok).collect::<Vec<_>>() | ||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by curiosity: am I correct saying that user could also give the hash instead of the encoded leaf here ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the leaf has to be encoded in it's
compact
form, so depending on the types used in the pallet it either has to be the hash (if it's wrapped inCompact
) or the full leaf. I don't think passing a hash here would work in case your MMR leaf is configured withoutCompact
(i.e. the "compact" encoding and regular encoding is the same).