Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

sp_trie proof compression primitives #8031

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,7 @@ zeroize = { opt-level = 3 }
[profile.release]
# Substrate runtime requires unwinding.
panic = "unwind"

[patch.crates-io]
trie-db = { git = 'https://github.com/cheme/trie', branch = 'skip_compact_values_alt' }
hash-db = { git = 'https://github.com/cheme/trie', branch = 'skip_compact_values_alt' }
29 changes: 27 additions & 2 deletions primitives/state-machine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1403,12 +1403,36 @@ mod tests {

#[test]
fn prove_read_and_proof_check_works() {
fn test_compact(remote_proof: StorageProof, remote_root: &sp_core::H256) -> StorageProof {
type Layout = sp_trie::Layout<BlakeTwo256>;
let compact_remote_proof = sp_trie::encode_compact::<Layout, _>(
remote_proof,
remote_root.clone(),
std::iter::empty(),
).unwrap();
let mut db = sp_trie::MemoryDB::<BlakeTwo256>::new(&[]);
sp_trie::decode_compact::<Layout, _, _, _, std::iter::Empty<_>>(
&mut db,
compact_remote_proof.iter_compact_encoded_nodes(),
(),
None,
Some(remote_root),
).unwrap();
StorageProof::new(db.drain().into_iter().filter_map(|kv|
if (kv.1).1 > 0 {
Some((kv.1).0)
} else {
None
}
).collect())
}
let child_info = ChildInfo::new_default(b"sub1");
let child_info = &child_info;
// fetch read proof from 'remote' full node
let remote_backend = trie_backend::tests::test_trie();
let remote_root = remote_backend.storage_root(::std::iter::empty()).0;
let remote_root = remote_backend.storage_root(std::iter::empty()).0;
let remote_proof = prove_read(remote_backend, &[b"value2"]).unwrap();
let remote_proof = test_compact(remote_proof, &remote_root);
// check proof locally
let local_result1 = read_proof_check::<BlakeTwo256, _>(
remote_root,
Expand All @@ -1428,12 +1452,13 @@ mod tests {
assert_eq!(local_result2, false);
// on child trie
let remote_backend = trie_backend::tests::test_trie();
let remote_root = remote_backend.storage_root(::std::iter::empty()).0;
let remote_root = remote_backend.storage_root(std::iter::empty()).0;
let remote_proof = prove_child_read(
remote_backend,
child_info,
&[b"value3"],
).unwrap();
let remote_proof = test_compact(remote_proof, &remote_root);
let local_result1 = read_child_proof_check::<BlakeTwo256, _>(
remote_root,
remote_proof.clone(),
Expand Down
2 changes: 1 addition & 1 deletion primitives/trie/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ harness = false
codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false }
sp-std = { version = "2.0.0", default-features = false, path = "../std" }
hash-db = { version = "0.15.2", default-features = false }
trie-db = { version = "0.22.2", default-features = false }
trie-db = { version = "0.22.3", default-features = false }
trie-root = { version = "0.16.0", default-features = false }
memory-db = { version = "0.26.0", default-features = false }
sp-core = { version = "2.0.0", default-features = false, path = "../core" }
Expand Down
7 changes: 6 additions & 1 deletion primitives/trie/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod error;
mod node_header;
mod node_codec;
mod storage_proof;
mod trie_codec;
mod trie_stream;

use sp_std::{boxed::Box, marker::PhantomData, vec::Vec, borrow::Borrow};
Expand All @@ -35,16 +36,20 @@ pub use error::Error;
pub use trie_stream::TrieStream;
/// The Substrate format implementation of `NodeCodec`.
pub use node_codec::NodeCodec;
pub use storage_proof::StorageProof;
pub use storage_proof::{StorageProof, CompactProof};
/// Various re-exports from the `trie-db` crate.
pub use trie_db::{
Trie, TrieMut, DBValue, Recorder, CError, Query, TrieLayout, TrieConfiguration, nibble_ops, TrieDBIterator,
LazyFetcher,
};
/// Various re-exports from the `memory-db` crate.
pub use memory_db::KeyFunction;
pub use memory_db::prefixed_key;
/// Various re-exports from the `hash-db` crate.
pub use hash_db::{HashDB as HashDBT, EMPTY_PREFIX};
/// Trie codec reexport, mainly child trie support
/// for trie compact proof.
pub use trie_codec::{decode_compact, encode_compact};

#[derive(Default)]
/// substrate trie layout
Expand Down
13 changes: 13 additions & 0 deletions primitives/trie/src/storage_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ pub struct StorageProof {
trie_nodes: Vec<Vec<u8>>,
}

/// Storage proof in compact form.
#[derive(Debug, PartialEq, Eq, Clone, Encode, Decode)]
pub struct CompactProof {
pub encoded_nodes: Vec<Vec<u8>>,
}

impl StorageProof {
/// Constructs a storage proof from a subset of encoded trie nodes in a storage backend.
pub fn new(trie_nodes: Vec<Vec<u8>>) -> Self {
Expand Down Expand Up @@ -77,6 +83,13 @@ impl StorageProof {
}
}

impl CompactProof {
/// Return an iterator on the compact encoded nodes.
pub fn iter_compact_encoded_nodes(&self) -> impl Iterator<Item = &[u8]> {
self.encoded_nodes.iter().map(Vec::as_slice)
}
}

/// An iterator over trie nodes constructed from a storage proof. The nodes are not guaranteed to
/// be traversed in any particular order.
pub struct StorageProofNodeIterator {
Expand Down
208 changes: 208 additions & 0 deletions primitives/trie/src/trie_codec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// This file is part of Substrate.

// Copyright (C) 2021-2021 Parity Technologies (UK) Ltd.
// 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.

//! Compact proof support.
//!
//! This uses compact proof from trie crate and extends
//! it to substrate specific layout and child trie system.

use crate::{EMPTY_PREFIX, HashDBT, LazyFetcher,
TrieHash, TrieError, TrieConfiguration, CompactProof, StorageProof};
use sp_std::boxed::Box;
use sp_std::vec::Vec;

type VerifyError<L> = crate::VerifyError<TrieHash<L>, Box<TrieError<L>>>;

fn verify_error<L: TrieConfiguration>(error: Box<TrieError<L>>) -> VerifyError<L> {
VerifyError::<L>::DecodeError(error)
}

/// Decode a compact proof.
///
/// Takes as input a destination `db` for decoded node and `encoded`
/// an iterator of compact encoded nodes.
///
/// Also allows optionally injecting specified value in
/// top trie proof with `know_keys` and the lazy
/// associated `fetcher`.
///
/// Child trie are decoded in order of child trie root present
/// in the top trie.
pub fn decode_compact<'a, L, DB, I, F, K>(
db: &mut DB,
encoded: I,
fetcher: F,
known_keys: Option<K>,
expected_root: Option<&TrieHash<L>>,
) -> Result<TrieHash<L>, VerifyError<L>>
where
L: TrieConfiguration,
DB: HashDBT<L::Hash, trie_db::DBValue> + hash_db::HashDBRef<L::Hash, trie_db::DBValue>,
I: IntoIterator<Item = &'a [u8]>,
F: LazyFetcher<'a>,
K: IntoIterator<Item = &'a [u8]>,
{
let mut nodes_iter = encoded.into_iter();
let (top_root, _nb_used) = if let Some(known_keys) = known_keys {
trie_db::decode_compact_with_known_values::<L, _, _, _, _, _>(
db,
&mut nodes_iter,
fetcher,
known_keys,
false, // current use of compact do to escape empty value.
).map_err(verify_error::<L>)?
} else {
trie_db::decode_compact_from_iter::<L, _, _, _>(
db,
&mut nodes_iter,
).map_err(verify_error::<L>)?
};

if let Some(expected_root) = expected_root {
if expected_root != &top_root {
return Err(VerifyError::<L>::RootMismatch(expected_root.clone()));
}
}

let mut child_tries = Vec::new();
{
// fetch child trie roots
let trie = crate::TrieDB::<L>::new(db, &top_root).map_err(verify_error::<L>)?;

use trie_db::Trie;
let mut iter = trie.iter().map_err(verify_error::<L>)?;

let childtrie_roots = sp_core::storage::well_known_keys::DEFAULT_CHILD_STORAGE_KEY_PREFIX;
if iter.seek(childtrie_roots).is_ok() {
loop {
match iter.next() {
Some(Ok((key, value))) if key.starts_with(childtrie_roots) => {
// we expect all default child trie root to be correctly encoded.
// see other child trie functions.
let mut root = TrieHash::<L>::default();
// still in a proof so prevent panic
if root.as_mut().len() != value.as_slice().len() {
return Err(VerifyError::<L>::RootMismatch(Default::default()));
}
root.as_mut().copy_from_slice(value.as_ref());
child_tries.push(root);
},
// allow incomplete database error: we only
// require access to data in the proof.
Some(Err(error)) => match *error {
trie_db::TrieError::IncompleteDatabase(..) => (),
e => return Err(VerifyError::<L>::DecodeError(Box::new(e))),
},
_ => break,
}
}
}
}

if !HashDBT::<L::Hash, _>::contains(db, &top_root, EMPTY_PREFIX) {
return Err(VerifyError::<L>::IncompleteProof);
}

let mut previous_extracted_child_trie = None;
for child_root in child_tries.into_iter() {
if previous_extracted_child_trie == None {
let (top_root, _) = trie_db::decode_compact_from_iter::<L, _, _, _>(
db,
&mut nodes_iter,
).map_err(verify_error::<L>)?;
previous_extracted_child_trie = Some(top_root);
}

// we allow skipping child root by only
// decoding next on match.
if Some(child_root) == previous_extracted_child_trie {
previous_extracted_child_trie = None;
}
}
if let Some(child_root) = previous_extracted_child_trie {
return Err(VerifyError::<L>::RootMismatch(child_root));
}

if nodes_iter.next().is_some() {
return Err(VerifyError::<L>::ExtraneousNode);
}

Ok(top_root)
}

pub fn encode_compact<'a, L, I>(
proof: StorageProof,
root: TrieHash<L>,
to_skip: I,
) -> Result<CompactProof, Box<TrieError<L>>>
where
L: TrieConfiguration,
I: IntoIterator<Item = &'a [u8]> + 'a,
{
let mut child_tries = Vec::new();
let partial_db = proof.into_memory_db();
let mut compact_proof = {
let trie = crate::TrieDB::<L>::new(&partial_db, &root)?;

use trie_db::Trie;
let mut iter = trie.iter()?;

let childtrie_roots = sp_core::storage::well_known_keys::DEFAULT_CHILD_STORAGE_KEY_PREFIX;
if iter.seek(childtrie_roots).is_ok() {
loop {
match iter.next() {
Some(Ok((key, value))) if key.starts_with(childtrie_roots) => {
let mut root = TrieHash::<L>::default();
if root.as_mut().len() != value.as_slice().len() {
return Err(Box::new(trie_db::TrieError::InvalidStateRoot(Default::default())));
}
root.as_mut().copy_from_slice(value.as_ref());
child_tries.push(root);
},
// allow incomplete database error: we only
// require access to data in the proof.
Some(Err(error)) => match *error {
trie_db::TrieError::IncompleteDatabase(..) => (),
e => return Err(Box::new(e)),
},
_ => break,
}
}
}

trie_db::encode_compact_skip_conditional_with_key::<L, _>(
&trie,
trie_db::compact_conditions::skip_given_ordered_keys(to_skip),
false, // We do not escape empty value.
)?
};

for child_root in child_tries {
if !HashDBT::<L::Hash, _>::contains(&partial_db, &child_root, EMPTY_PREFIX) {
// child proof are allowed to be missing (unused root can be included
// due to trie structure modification).
continue;
}

let trie = crate::TrieDB::<L>::new(&partial_db, &child_root)?;
let child_proof = trie_db::encode_compact::<L>(&trie)?;

compact_proof.extend(child_proof);
}

Ok(CompactProof { encoded_nodes: compact_proof })
}
2 changes: 1 addition & 1 deletion utils/wasm-builder/src/wasm_project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ fn create_project_cargo_toml(
wasm_workspace_toml.insert("profile".into(), profile.into());

// Add patch section from the project root `Cargo.toml`
if let Some(mut patch) = workspace_toml.remove("patch").and_then(|p| p.try_into::<Table>().ok()) {
while let Some(mut patch) = workspace_toml.remove("patch").and_then(|p| p.try_into::<Table>().ok()) {
// Iterate over all patches and make the patch path absolute from the workspace root path.
patch.iter_mut()
.filter_map(|p|
Expand Down