mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-07-22 02:05:41 +00:00
Fast sync child trie support. (#9239)
* state machine proofs. * initial implementation * Remove todo. * Extend test and fix import. * fix no proof, with proof ko. * fix start at logic. * Restore response size. * Rework comments. * Add explicit ref * Use compact proof. * ref change * elaborato on empty change set condition. * KeyValueState renaming. * Do not add two time child trie with same root to sync reply. * rust format * Fix merge. * fix warnings and fmt * fmt * update protocol id to V2
This commit is contained in:
@@ -42,7 +42,7 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use sp_state_machine::Backend as StateBackend;
|
pub use sp_state_machine::{Backend as StateBackend, KeyValueStates};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
/// Extracts the state backend type for the given backend.
|
/// Extracts the state backend type for the given backend.
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ pub use proof_provider::*;
|
|||||||
pub use sp_blockchain as blockchain;
|
pub use sp_blockchain as blockchain;
|
||||||
pub use sp_blockchain::HeaderBackend;
|
pub use sp_blockchain::HeaderBackend;
|
||||||
|
|
||||||
pub use sp_state_machine::{ExecutionStrategy, StorageProof};
|
pub use sp_state_machine::{CompactProof, ExecutionStrategy, StorageProof};
|
||||||
pub use sp_storage::{ChildInfo, PrefixedStorageKey, StorageData, StorageKey};
|
pub use sp_storage::{ChildInfo, PrefixedStorageKey, StorageData, StorageKey};
|
||||||
|
|
||||||
/// Usage Information Provider interface
|
/// Usage Information Provider interface
|
||||||
|
|||||||
@@ -17,8 +17,9 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
//! Proof utilities
|
//! Proof utilities
|
||||||
use crate::{ChangesProof, StorageProof};
|
use crate::{ChangesProof, CompactProof, StorageProof};
|
||||||
use sp_runtime::{generic::BlockId, traits::Block as BlockT};
|
use sp_runtime::{generic::BlockId, traits::Block as BlockT};
|
||||||
|
use sp_state_machine::{KeyValueStates, KeyValueStorageLevel};
|
||||||
use sp_storage::{ChildInfo, PrefixedStorageKey, StorageKey};
|
use sp_storage::{ChildInfo, PrefixedStorageKey, StorageKey};
|
||||||
|
|
||||||
/// Interface for providing block proving utilities.
|
/// Interface for providing block proving utilities.
|
||||||
@@ -71,31 +72,43 @@ pub trait ProofProvider<Block: BlockT> {
|
|||||||
key: &StorageKey,
|
key: &StorageKey,
|
||||||
) -> sp_blockchain::Result<ChangesProof<Block::Header>>;
|
) -> sp_blockchain::Result<ChangesProof<Block::Header>>;
|
||||||
|
|
||||||
/// Given a `BlockId` iterate over all storage values starting at `start_key` exclusively,
|
/// Given a `BlockId` iterate over all storage values starting at `start_keys`.
|
||||||
/// building proofs until size limit is reached. Returns combined proof and the number of
|
/// Last `start_keys` element contains last accessed key value.
|
||||||
/// collected keys.
|
/// With multiple `start_keys`, first `start_keys` element is
|
||||||
|
/// the current storage key of of the last accessed child trie.
|
||||||
|
/// at last level the value to start at exclusively.
|
||||||
|
/// Proofs is build until size limit is reached and always include at
|
||||||
|
/// least one key following `start_keys`.
|
||||||
|
/// Returns combined proof and the numbers of collected keys.
|
||||||
fn read_proof_collection(
|
fn read_proof_collection(
|
||||||
&self,
|
&self,
|
||||||
id: &BlockId<Block>,
|
id: &BlockId<Block>,
|
||||||
start_key: &[u8],
|
start_keys: &[Vec<u8>],
|
||||||
size_limit: usize,
|
size_limit: usize,
|
||||||
) -> sp_blockchain::Result<(StorageProof, u32)>;
|
) -> sp_blockchain::Result<(CompactProof, u32)>;
|
||||||
|
|
||||||
/// Given a `BlockId` iterate over all storage values starting at `start_key`.
|
/// Given a `BlockId` iterate over all storage values starting at `start_key`.
|
||||||
/// Returns collected keys and values.
|
/// Returns collected keys and values.
|
||||||
|
/// Returns the collected keys values content of the top trie followed by the
|
||||||
|
/// collected keys values of child tries.
|
||||||
|
/// Only child tries with their root part of the collected content or
|
||||||
|
/// related to `start_key` are attached.
|
||||||
|
/// For each collected state a boolean indicates if state reach
|
||||||
|
/// end.
|
||||||
fn storage_collection(
|
fn storage_collection(
|
||||||
&self,
|
&self,
|
||||||
id: &BlockId<Block>,
|
id: &BlockId<Block>,
|
||||||
start_key: &[u8],
|
start_key: &[Vec<u8>],
|
||||||
size_limit: usize,
|
size_limit: usize,
|
||||||
) -> sp_blockchain::Result<Vec<(Vec<u8>, Vec<u8>)>>;
|
) -> sp_blockchain::Result<Vec<(KeyValueStorageLevel, bool)>>;
|
||||||
|
|
||||||
/// Verify read storage proof for a set of keys.
|
/// Verify read storage proof for a set of keys.
|
||||||
/// Returns collected key-value pairs and a flag indicating if iteration is complete.
|
/// Returns collected key-value pairs and a the nested state
|
||||||
|
/// depth of current iteration or 0 if completed.
|
||||||
fn verify_range_proof(
|
fn verify_range_proof(
|
||||||
&self,
|
&self,
|
||||||
root: Block::Hash,
|
root: Block::Hash,
|
||||||
proof: StorageProof,
|
proof: CompactProof,
|
||||||
start_key: &[u8],
|
start_keys: &[Vec<u8>],
|
||||||
) -> sp_blockchain::Result<(Vec<(Vec<u8>, Vec<u8>)>, bool)>;
|
) -> sp_blockchain::Result<(KeyValueStates, usize)>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ pub struct ImportedState<B: BlockT> {
|
|||||||
/// Target block hash.
|
/// Target block hash.
|
||||||
pub block: B::Hash,
|
pub block: B::Hash,
|
||||||
/// State keys and values.
|
/// State keys and values.
|
||||||
pub state: Vec<(Vec<u8>, Vec<u8>)>,
|
pub state: sp_state_machine::KeyValueStates,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<B: BlockT> std::fmt::Debug for ImportedState<B> {
|
impl<B: BlockT> std::fmt::Debug for ImportedState<B> {
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use codec::{Decode, Encode};
|
use codec::{Decode, Encode};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use sc_client_api::StorageProof;
|
use sc_client_api::CompactProof;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use sp_core::storage::well_known_keys;
|
||||||
use sp_runtime::traits::{Block as BlockT, Header, NumberFor};
|
use sp_runtime::traits::{Block as BlockT, Header, NumberFor};
|
||||||
use std::sync::Arc;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
/// State sync support.
|
/// State sync support.
|
||||||
|
|
||||||
@@ -35,8 +37,8 @@ pub struct StateSync<B: BlockT> {
|
|||||||
target_block: B::Hash,
|
target_block: B::Hash,
|
||||||
target_header: B::Header,
|
target_header: B::Header,
|
||||||
target_root: B::Hash,
|
target_root: B::Hash,
|
||||||
last_key: Vec<u8>,
|
last_key: SmallVec<[Vec<u8>; 2]>,
|
||||||
state: Vec<(Vec<u8>, Vec<u8>)>,
|
state: HashMap<Vec<u8>, (Vec<(Vec<u8>, Vec<u8>)>, Vec<Vec<u8>>)>,
|
||||||
complete: bool,
|
complete: bool,
|
||||||
client: Arc<dyn Client<B>>,
|
client: Arc<dyn Client<B>>,
|
||||||
imported_bytes: u64,
|
imported_bytes: u64,
|
||||||
@@ -61,8 +63,8 @@ impl<B: BlockT> StateSync<B> {
|
|||||||
target_block: target.hash(),
|
target_block: target.hash(),
|
||||||
target_root: target.state_root().clone(),
|
target_root: target.state_root().clone(),
|
||||||
target_header: target,
|
target_header: target,
|
||||||
last_key: Vec::default(),
|
last_key: SmallVec::default(),
|
||||||
state: Vec::default(),
|
state: HashMap::default(),
|
||||||
complete: false,
|
complete: false,
|
||||||
imported_bytes: 0,
|
imported_bytes: 0,
|
||||||
skip_proof,
|
skip_proof,
|
||||||
@@ -71,7 +73,7 @@ impl<B: BlockT> StateSync<B> {
|
|||||||
|
|
||||||
/// Validate and import a state reponse.
|
/// Validate and import a state reponse.
|
||||||
pub fn import(&mut self, response: StateResponse) -> ImportResult<B> {
|
pub fn import(&mut self, response: StateResponse) -> ImportResult<B> {
|
||||||
if response.entries.is_empty() && response.proof.is_empty() && !response.complete {
|
if response.entries.is_empty() && response.proof.is_empty() {
|
||||||
debug!(target: "sync", "Bad state response");
|
debug!(target: "sync", "Bad state response");
|
||||||
return ImportResult::BadResponse
|
return ImportResult::BadResponse
|
||||||
}
|
}
|
||||||
@@ -82,56 +84,135 @@ impl<B: BlockT> StateSync<B> {
|
|||||||
let complete = if !self.skip_proof {
|
let complete = if !self.skip_proof {
|
||||||
debug!(target: "sync", "Importing state from {} trie nodes", response.proof.len());
|
debug!(target: "sync", "Importing state from {} trie nodes", response.proof.len());
|
||||||
let proof_size = response.proof.len() as u64;
|
let proof_size = response.proof.len() as u64;
|
||||||
let proof = match StorageProof::decode(&mut response.proof.as_ref()) {
|
let proof = match CompactProof::decode(&mut response.proof.as_ref()) {
|
||||||
Ok(proof) => proof,
|
Ok(proof) => proof,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!(target: "sync", "Error decoding proof: {:?}", e);
|
debug!(target: "sync", "Error decoding proof: {:?}", e);
|
||||||
return ImportResult::BadResponse
|
return ImportResult::BadResponse
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let (values, complete) =
|
let (values, completed) = match self.client.verify_range_proof(
|
||||||
match self.client.verify_range_proof(self.target_root, proof, &self.last_key) {
|
self.target_root,
|
||||||
Err(e) => {
|
proof,
|
||||||
debug!(target: "sync", "StateResponse failed proof verification: {:?}", e);
|
self.last_key.as_slice(),
|
||||||
return ImportResult::BadResponse
|
) {
|
||||||
},
|
Err(e) => {
|
||||||
Ok(values) => values,
|
debug!(
|
||||||
};
|
target: "sync",
|
||||||
|
"StateResponse failed proof verification: {:?}",
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
return ImportResult::BadResponse
|
||||||
|
},
|
||||||
|
Ok(values) => values,
|
||||||
|
};
|
||||||
debug!(target: "sync", "Imported with {} keys", values.len());
|
debug!(target: "sync", "Imported with {} keys", values.len());
|
||||||
|
|
||||||
if let Some(last) = values.last().map(|(k, _)| k) {
|
let complete = completed == 0;
|
||||||
self.last_key = last.clone();
|
if !complete && !values.update_last_key(completed, &mut self.last_key) {
|
||||||
}
|
debug!(target: "sync", "Error updating key cursor, depth: {}", completed);
|
||||||
|
};
|
||||||
|
|
||||||
for (key, value) in values {
|
for values in values.0 {
|
||||||
self.imported_bytes += key.len() as u64;
|
let key_values = if values.state_root.is_empty() {
|
||||||
self.state.push((key, value))
|
// Read child trie roots.
|
||||||
|
values
|
||||||
|
.key_values
|
||||||
|
.into_iter()
|
||||||
|
.filter(|key_value| {
|
||||||
|
if well_known_keys::is_child_storage_key(key_value.0.as_slice()) {
|
||||||
|
self.state
|
||||||
|
.entry(key_value.1.clone())
|
||||||
|
.or_default()
|
||||||
|
.1
|
||||||
|
.push(key_value.0.clone());
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
values.key_values
|
||||||
|
};
|
||||||
|
let mut entry = self.state.entry(values.state_root).or_default();
|
||||||
|
if entry.0.len() > 0 && entry.1.len() > 1 {
|
||||||
|
// Already imported child_trie with same root.
|
||||||
|
// Warning this will not work with parallel download.
|
||||||
|
} else {
|
||||||
|
if entry.0.is_empty() {
|
||||||
|
for (key, _value) in key_values.iter() {
|
||||||
|
self.imported_bytes += key.len() as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.0 = key_values;
|
||||||
|
} else {
|
||||||
|
for (key, value) in key_values {
|
||||||
|
self.imported_bytes += key.len() as u64;
|
||||||
|
entry.0.push((key, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.imported_bytes += proof_size;
|
self.imported_bytes += proof_size;
|
||||||
complete
|
complete
|
||||||
} else {
|
} else {
|
||||||
debug!(
|
let mut complete = true;
|
||||||
target: "sync",
|
// if the trie is a child trie and one of its parent trie is empty,
|
||||||
"Importing state from {:?} to {:?}",
|
// the parent cursor stays valid.
|
||||||
response.entries.last().map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key)),
|
// Empty parent trie content only happens when all the response content
|
||||||
response.entries.first().map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key)),
|
// is part of a single child trie.
|
||||||
);
|
if self.last_key.len() == 2 && response.entries[0].entries.len() == 0 {
|
||||||
|
// Do not remove the parent trie position.
|
||||||
|
self.last_key.pop();
|
||||||
|
} else {
|
||||||
|
self.last_key.clear();
|
||||||
|
}
|
||||||
|
for state in response.entries {
|
||||||
|
debug!(
|
||||||
|
target: "sync",
|
||||||
|
"Importing state from {:?} to {:?}",
|
||||||
|
state.entries.last().map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key)),
|
||||||
|
state.entries.first().map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key)),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(e) = response.entries.last() {
|
if !state.complete {
|
||||||
self.last_key = e.key.clone();
|
if let Some(e) = state.entries.last() {
|
||||||
|
self.last_key.push(e.key.clone());
|
||||||
|
}
|
||||||
|
complete = false;
|
||||||
|
}
|
||||||
|
let is_top = state.state_root.is_empty();
|
||||||
|
let entry = self.state.entry(state.state_root).or_default();
|
||||||
|
if entry.0.len() > 0 && entry.1.len() > 1 {
|
||||||
|
// Already imported child trie with same root.
|
||||||
|
} else {
|
||||||
|
let mut child_roots = Vec::new();
|
||||||
|
for StateEntry { key, value } in state.entries {
|
||||||
|
// Skip all child key root (will be recalculated on import).
|
||||||
|
if is_top && well_known_keys::is_child_storage_key(key.as_slice()) {
|
||||||
|
child_roots.push((value, key));
|
||||||
|
} else {
|
||||||
|
self.imported_bytes += key.len() as u64;
|
||||||
|
entry.0.push((key, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (root, storage_key) in child_roots {
|
||||||
|
self.state.entry(root).or_default().1.push(storage_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for StateEntry { key, value } in response.entries {
|
complete
|
||||||
self.imported_bytes += (key.len() + value.len()) as u64;
|
|
||||||
self.state.push((key, value))
|
|
||||||
}
|
|
||||||
response.complete
|
|
||||||
};
|
};
|
||||||
if complete {
|
if complete {
|
||||||
self.complete = true;
|
self.complete = true;
|
||||||
ImportResult::Import(
|
ImportResult::Import(
|
||||||
self.target_block,
|
self.target_block,
|
||||||
self.target_header.clone(),
|
self.target_header.clone(),
|
||||||
ImportedState { block: self.target_block, state: std::mem::take(&mut self.state) },
|
ImportedState {
|
||||||
|
block: self.target_block.clone(),
|
||||||
|
state: std::mem::take(&mut self.state).into(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ImportResult::Continue
|
ImportResult::Continue
|
||||||
@@ -142,7 +223,7 @@ impl<B: BlockT> StateSync<B> {
|
|||||||
pub fn next_request(&self) -> StateRequest {
|
pub fn next_request(&self) -> StateRequest {
|
||||||
StateRequest {
|
StateRequest {
|
||||||
block: self.target_block.encode(),
|
block: self.target_block.encode(),
|
||||||
start: self.last_key.clone(),
|
start: self.last_key.clone().into_vec(),
|
||||||
no_proof: self.skip_proof,
|
no_proof: self.skip_proof,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,7 +245,8 @@ impl<B: BlockT> StateSync<B> {
|
|||||||
|
|
||||||
/// Returns state sync estimated progress.
|
/// Returns state sync estimated progress.
|
||||||
pub fn progress(&self) -> StateDownloadProgress {
|
pub fn progress(&self) -> StateDownloadProgress {
|
||||||
let percent_done = (*self.last_key.get(0).unwrap_or(&0u8) as u32) * 100 / 256;
|
let cursor = *self.last_key.get(0).and_then(|last| last.get(0)).unwrap_or(&0u8);
|
||||||
|
let percent_done = cursor as u32 * 100 / 256;
|
||||||
StateDownloadProgress { percentage: percent_done, size: self.imported_bytes }
|
StateDownloadProgress { percentage: percent_done, size: self.imported_bytes }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,22 +74,32 @@ message BlockData {
|
|||||||
message StateRequest {
|
message StateRequest {
|
||||||
// Block header hash.
|
// Block header hash.
|
||||||
bytes block = 1;
|
bytes block = 1;
|
||||||
// Start from this key. Equivalent to <empty bytes> if omitted.
|
// Start from this key.
|
||||||
bytes start = 2; // optional
|
// Multiple keys used for nested state start.
|
||||||
|
repeated bytes start = 2; // optional
|
||||||
// if 'true' indicates that response should contain raw key-values, rather than proof.
|
// if 'true' indicates that response should contain raw key-values, rather than proof.
|
||||||
bool no_proof = 3;
|
bool no_proof = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message StateResponse {
|
message StateResponse {
|
||||||
// A collection of keys-values. Only populated if `no_proof` is `true`
|
// A collection of keys-values states. Only populated if `no_proof` is `true`
|
||||||
repeated StateEntry entries = 1;
|
repeated KeyValueStateEntry entries = 1;
|
||||||
// If `no_proof` is false in request, this contains proof nodes.
|
// If `no_proof` is false in request, this contains proof nodes.
|
||||||
bytes proof = 2;
|
bytes proof = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A key value state.
|
||||||
|
message KeyValueStateEntry {
|
||||||
|
// Root of for this level, empty length bytes
|
||||||
|
// if top level.
|
||||||
|
bytes state_root = 1;
|
||||||
|
// A collection of keys-values.
|
||||||
|
repeated StateEntry entries = 2;
|
||||||
// Set to true when there are no more keys to return.
|
// Set to true when there are no more keys to return.
|
||||||
bool complete = 3;
|
bool complete = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A key-value pair
|
// A key-value pair.
|
||||||
message StateEntry {
|
message StateEntry {
|
||||||
bytes key = 1;
|
bytes key = 1;
|
||||||
bytes value = 2;
|
bytes value = 2;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use crate::{
|
|||||||
chain::Client,
|
chain::Client,
|
||||||
config::ProtocolId,
|
config::ProtocolId,
|
||||||
request_responses::{IncomingRequest, OutgoingResponse, ProtocolConfig},
|
request_responses::{IncomingRequest, OutgoingResponse, ProtocolConfig},
|
||||||
schema::v1::{StateEntry, StateRequest, StateResponse},
|
schema::v1::{KeyValueStateEntry, StateEntry, StateRequest, StateResponse},
|
||||||
PeerId, ReputationChange,
|
PeerId, ReputationChange,
|
||||||
};
|
};
|
||||||
use codec::{Decode, Encode};
|
use codec::{Decode, Encode};
|
||||||
@@ -66,7 +66,7 @@ fn generate_protocol_name(protocol_id: &ProtocolId) -> String {
|
|||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
s.push_str("/");
|
s.push_str("/");
|
||||||
s.push_str(protocol_id.as_ref());
|
s.push_str(protocol_id.as_ref());
|
||||||
s.push_str("/state/1");
|
s.push_str("/state/2");
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ fn generate_protocol_name(protocol_id: &ProtocolId) -> String {
|
|||||||
struct SeenRequestsKey<B: BlockT> {
|
struct SeenRequestsKey<B: BlockT> {
|
||||||
peer: PeerId,
|
peer: PeerId,
|
||||||
block: B::Hash,
|
block: B::Hash,
|
||||||
start: Vec<u8>,
|
start: Vec<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::derive_hash_xor_eq)]
|
#[allow(clippy::derive_hash_xor_eq)]
|
||||||
@@ -169,10 +169,10 @@ impl<B: BlockT> StateRequestHandler<B> {
|
|||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
target: LOG_TARGET,
|
target: LOG_TARGET,
|
||||||
"Handling state request from {}: Block {:?}, Starting at {:?}, no_proof={}",
|
"Handling state request from {}: Block {:?}, Starting at {:x?}, no_proof={}",
|
||||||
peer,
|
peer,
|
||||||
request.block,
|
request.block,
|
||||||
sp_core::hexdisplay::HexDisplay::from(&request.start),
|
&request.start,
|
||||||
request.no_proof,
|
request.no_proof,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -180,36 +180,45 @@ impl<B: BlockT> StateRequestHandler<B> {
|
|||||||
let mut response = StateResponse::default();
|
let mut response = StateResponse::default();
|
||||||
|
|
||||||
if !request.no_proof {
|
if !request.no_proof {
|
||||||
let (proof, count) = self.client.read_proof_collection(
|
let (proof, _count) = self.client.read_proof_collection(
|
||||||
&BlockId::hash(block),
|
&BlockId::hash(block),
|
||||||
&request.start,
|
request.start.as_slice(),
|
||||||
MAX_RESPONSE_BYTES,
|
MAX_RESPONSE_BYTES,
|
||||||
)?;
|
)?;
|
||||||
response.proof = proof.encode();
|
response.proof = proof.encode();
|
||||||
if count == 0 {
|
|
||||||
response.complete = true;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let entries = self.client.storage_collection(
|
let entries = self.client.storage_collection(
|
||||||
&BlockId::hash(block),
|
&BlockId::hash(block),
|
||||||
&request.start,
|
request.start.as_slice(),
|
||||||
MAX_RESPONSE_BYTES,
|
MAX_RESPONSE_BYTES,
|
||||||
)?;
|
)?;
|
||||||
response.entries =
|
response.entries = entries
|
||||||
entries.into_iter().map(|(key, value)| StateEntry { key, value }).collect();
|
.into_iter()
|
||||||
if response.entries.is_empty() {
|
.map(|(state, complete)| KeyValueStateEntry {
|
||||||
response.complete = true;
|
state_root: state.state_root,
|
||||||
}
|
entries: state
|
||||||
|
.key_values
|
||||||
|
.into_iter()
|
||||||
|
.map(|(key, value)| StateEntry { key, value })
|
||||||
|
.collect(),
|
||||||
|
complete,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
target: LOG_TARGET,
|
target: LOG_TARGET,
|
||||||
"StateResponse contains {} keys, {}, proof nodes, complete={}, from {:?} to {:?}",
|
"StateResponse contains {} keys, {}, proof nodes, from {:?} to {:?}",
|
||||||
response.entries.len(),
|
response.entries.len(),
|
||||||
response.proof.len(),
|
response.proof.len(),
|
||||||
response.complete,
|
response.entries.get(0).and_then(|top| top
|
||||||
response.entries.first().map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key)),
|
.entries
|
||||||
response.entries.last().map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key)),
|
.first()
|
||||||
|
.map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key))),
|
||||||
|
response.entries.get(0).and_then(|top| top
|
||||||
|
.entries
|
||||||
|
.last()
|
||||||
|
.map(|e| sp_core::hexdisplay::HexDisplay::from(&e.key))),
|
||||||
);
|
);
|
||||||
if let Some(value) = self.seen_requests.get_mut(&key) {
|
if let Some(value) = self.seen_requests.get_mut(&key) {
|
||||||
// If this is the first time we have processed this request, we need to change
|
// If this is the first time we have processed this request, we need to change
|
||||||
|
|||||||
@@ -697,6 +697,8 @@ pub struct FullPeerConfig {
|
|||||||
pub is_authority: bool,
|
pub is_authority: bool,
|
||||||
/// Syncing mode
|
/// Syncing mode
|
||||||
pub sync_mode: SyncMode,
|
pub sync_mode: SyncMode,
|
||||||
|
/// Extra genesis storage.
|
||||||
|
pub extra_storage: Option<sp_core::storage::Storage>,
|
||||||
/// Enable transaction indexing.
|
/// Enable transaction indexing.
|
||||||
pub storage_chain: bool,
|
pub storage_chain: bool,
|
||||||
}
|
}
|
||||||
@@ -765,6 +767,11 @@ where
|
|||||||
(Some(keep_blocks), false) => TestClientBuilder::with_pruning_window(keep_blocks),
|
(Some(keep_blocks), false) => TestClientBuilder::with_pruning_window(keep_blocks),
|
||||||
(None, false) => TestClientBuilder::with_default_backend(),
|
(None, false) => TestClientBuilder::with_default_backend(),
|
||||||
};
|
};
|
||||||
|
if let Some(storage) = config.extra_storage {
|
||||||
|
let genesis_extra_storage = test_client_builder.genesis_init_mut().extra_storage();
|
||||||
|
*genesis_extra_storage = storage;
|
||||||
|
}
|
||||||
|
|
||||||
if matches!(config.sync_mode, SyncMode::Fast { .. } | SyncMode::Warp) {
|
if matches!(config.sync_mode, SyncMode::Fast { .. } | SyncMode::Warp) {
|
||||||
test_client_builder = test_client_builder.set_no_genesis();
|
test_client_builder = test_client_builder.set_no_genesis();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1110,11 +1110,44 @@ fn syncs_state() {
|
|||||||
sp_tracing::try_init_simple();
|
sp_tracing::try_init_simple();
|
||||||
for skip_proofs in &[false, true] {
|
for skip_proofs in &[false, true] {
|
||||||
let mut net = TestNet::new(0);
|
let mut net = TestNet::new(0);
|
||||||
net.add_full_peer_with_config(Default::default());
|
let mut genesis_storage: sp_core::storage::Storage = Default::default();
|
||||||
net.add_full_peer_with_config(FullPeerConfig {
|
genesis_storage.top.insert(b"additional_key".to_vec(), vec![1]);
|
||||||
sync_mode: SyncMode::Fast { skip_proofs: *skip_proofs, storage_chain_mode: false },
|
let mut child_data: std::collections::BTreeMap<Vec<u8>, Vec<u8>> = Default::default();
|
||||||
..Default::default()
|
for i in 0u8..16 {
|
||||||
});
|
child_data.insert(vec![i; 5], vec![i; 33]);
|
||||||
|
}
|
||||||
|
let child1 = sp_core::storage::StorageChild {
|
||||||
|
data: child_data.clone(),
|
||||||
|
child_info: sp_core::storage::ChildInfo::new_default(b"child1"),
|
||||||
|
};
|
||||||
|
let child3 = sp_core::storage::StorageChild {
|
||||||
|
data: child_data.clone(),
|
||||||
|
child_info: sp_core::storage::ChildInfo::new_default(b"child3"),
|
||||||
|
};
|
||||||
|
for i in 22u8..33 {
|
||||||
|
child_data.insert(vec![i; 5], vec![i; 33]);
|
||||||
|
}
|
||||||
|
let child2 = sp_core::storage::StorageChild {
|
||||||
|
data: child_data.clone(),
|
||||||
|
child_info: sp_core::storage::ChildInfo::new_default(b"child2"),
|
||||||
|
};
|
||||||
|
genesis_storage
|
||||||
|
.children_default
|
||||||
|
.insert(child1.child_info.storage_key().to_vec(), child1);
|
||||||
|
genesis_storage
|
||||||
|
.children_default
|
||||||
|
.insert(child2.child_info.storage_key().to_vec(), child2);
|
||||||
|
genesis_storage
|
||||||
|
.children_default
|
||||||
|
.insert(child3.child_info.storage_key().to_vec(), child3);
|
||||||
|
let mut config_one = FullPeerConfig::default();
|
||||||
|
config_one.extra_storage = Some(genesis_storage.clone());
|
||||||
|
net.add_full_peer_with_config(config_one);
|
||||||
|
let mut config_two = FullPeerConfig::default();
|
||||||
|
config_two.extra_storage = Some(genesis_storage);
|
||||||
|
config_two.sync_mode =
|
||||||
|
SyncMode::Fast { skip_proofs: *skip_proofs, storage_chain_mode: false };
|
||||||
|
net.add_full_peer_with_config(config_two);
|
||||||
net.peer(0).push_blocks(64, false);
|
net.peer(0).push_blocks(64, false);
|
||||||
// Wait for peer 1 to sync header chain.
|
// Wait for peer 1 to sync header chain.
|
||||||
net.block_until_sync();
|
net.block_until_sync();
|
||||||
|
|||||||
@@ -64,7 +64,10 @@ use sp_consensus::{BlockOrigin, BlockStatus, Error as ConsensusError};
|
|||||||
use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedSender};
|
use sc_utils::mpsc::{tracing_unbounded, TracingUnboundedSender};
|
||||||
use sp_core::{
|
use sp_core::{
|
||||||
convert_hash,
|
convert_hash,
|
||||||
storage::{well_known_keys, ChildInfo, PrefixedStorageKey, StorageData, StorageKey},
|
storage::{
|
||||||
|
well_known_keys, ChildInfo, ChildType, PrefixedStorageKey, StorageChild, StorageData,
|
||||||
|
StorageKey,
|
||||||
|
},
|
||||||
ChangesTrieConfiguration, NativeOrEncoded,
|
ChangesTrieConfiguration, NativeOrEncoded,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "test-helpers")]
|
#[cfg(feature = "test-helpers")]
|
||||||
@@ -78,11 +81,12 @@ use sp_runtime::{
|
|||||||
BuildStorage, Justification, Justifications,
|
BuildStorage, Justification, Justifications,
|
||||||
};
|
};
|
||||||
use sp_state_machine::{
|
use sp_state_machine::{
|
||||||
key_changes, key_changes_proof, prove_child_read, prove_range_read_with_size, prove_read,
|
key_changes, key_changes_proof, prove_child_read, prove_range_read_with_child_with_size,
|
||||||
read_range_proof_check, Backend as StateBackend, ChangesTrieAnchorBlockId,
|
prove_read, read_range_proof_check_with_child_on_proving_backend, Backend as StateBackend,
|
||||||
ChangesTrieConfigurationRange, ChangesTrieRootsStorage, ChangesTrieStorage, DBValue,
|
ChangesTrieAnchorBlockId, ChangesTrieConfigurationRange, ChangesTrieRootsStorage,
|
||||||
|
ChangesTrieStorage, DBValue, KeyValueStates, KeyValueStorageLevel, MAX_NESTED_TRIE_DEPTH,
|
||||||
};
|
};
|
||||||
use sp_trie::StorageProof;
|
use sp_trie::{CompactProof, StorageProof};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashMap, HashSet},
|
collections::{BTreeMap, HashMap, HashSet},
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
@@ -824,10 +828,37 @@ where
|
|||||||
Some((main_sc, child_sc))
|
Some((main_sc, child_sc))
|
||||||
},
|
},
|
||||||
sc_consensus::StorageChanges::Import(changes) => {
|
sc_consensus::StorageChanges::Import(changes) => {
|
||||||
let storage = sp_storage::Storage {
|
let mut storage = sp_storage::Storage::default();
|
||||||
top: changes.state.into_iter().collect(),
|
for state in changes.state.0.into_iter() {
|
||||||
children_default: Default::default(),
|
if state.parent_storage_keys.len() == 0 && state.state_root.len() == 0 {
|
||||||
};
|
for (key, value) in state.key_values.into_iter() {
|
||||||
|
storage.top.insert(key, value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for parent_storage in state.parent_storage_keys {
|
||||||
|
let storage_key = PrefixedStorageKey::new_ref(&parent_storage);
|
||||||
|
let storage_key =
|
||||||
|
match ChildType::from_prefixed_key(&storage_key) {
|
||||||
|
Some((ChildType::ParentKeyId, storage_key)) =>
|
||||||
|
storage_key,
|
||||||
|
None =>
|
||||||
|
return Err(Error::Backend(
|
||||||
|
"Invalid child storage key.".to_string(),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
let entry = storage
|
||||||
|
.children_default
|
||||||
|
.entry(storage_key.to_vec())
|
||||||
|
.or_insert_with(|| StorageChild {
|
||||||
|
data: Default::default(),
|
||||||
|
child_info: ChildInfo::new_default(storage_key),
|
||||||
|
});
|
||||||
|
for (key, value) in state.key_values.iter() {
|
||||||
|
entry.data.insert(key.clone(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let state_root = operation.op.reset_storage(storage)?;
|
let state_root = operation.op.reset_storage(storage)?;
|
||||||
if state_root != *import_headers.post().state_root() {
|
if state_root != *import_headers.post().state_root() {
|
||||||
@@ -1347,62 +1378,153 @@ where
|
|||||||
fn read_proof_collection(
|
fn read_proof_collection(
|
||||||
&self,
|
&self,
|
||||||
id: &BlockId<Block>,
|
id: &BlockId<Block>,
|
||||||
start_key: &[u8],
|
start_key: &[Vec<u8>],
|
||||||
size_limit: usize,
|
size_limit: usize,
|
||||||
) -> sp_blockchain::Result<(StorageProof, u32)> {
|
) -> sp_blockchain::Result<(CompactProof, u32)> {
|
||||||
let state = self.state_at(id)?;
|
let state = self.state_at(id)?;
|
||||||
Ok(prove_range_read_with_size::<_, HashFor<Block>>(
|
let root = state.storage_root(std::iter::empty()).0;
|
||||||
state,
|
|
||||||
None,
|
let (proof, count) = prove_range_read_with_child_with_size::<_, HashFor<Block>>(
|
||||||
None,
|
state, size_limit, start_key,
|
||||||
size_limit,
|
)?;
|
||||||
Some(start_key),
|
let proof = sp_trie::encode_compact::<sp_trie::Layout<HashFor<Block>>>(proof, root)
|
||||||
)?)
|
.map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?;
|
||||||
|
Ok((proof, count))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn storage_collection(
|
fn storage_collection(
|
||||||
&self,
|
&self,
|
||||||
id: &BlockId<Block>,
|
id: &BlockId<Block>,
|
||||||
start_key: &[u8],
|
start_key: &[Vec<u8>],
|
||||||
size_limit: usize,
|
size_limit: usize,
|
||||||
) -> sp_blockchain::Result<Vec<(Vec<u8>, Vec<u8>)>> {
|
) -> sp_blockchain::Result<Vec<(KeyValueStorageLevel, bool)>> {
|
||||||
|
if start_key.len() > MAX_NESTED_TRIE_DEPTH {
|
||||||
|
return Err(Error::Backend("Invalid start key.".to_string()))
|
||||||
|
}
|
||||||
let state = self.state_at(id)?;
|
let state = self.state_at(id)?;
|
||||||
let mut current_key = start_key.to_vec();
|
let child_info = |storage_key: &Vec<u8>| -> sp_blockchain::Result<ChildInfo> {
|
||||||
let mut total_size = 0;
|
let storage_key = PrefixedStorageKey::new_ref(&storage_key);
|
||||||
let mut entries = Vec::new();
|
match ChildType::from_prefixed_key(&storage_key) {
|
||||||
while let Some(next_key) = state
|
Some((ChildType::ParentKeyId, storage_key)) =>
|
||||||
.next_storage_key(¤t_key)
|
Ok(ChildInfo::new_default(storage_key)),
|
||||||
.map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?
|
None => Err(Error::Backend("Invalid child storage key.".to_string())),
|
||||||
{
|
}
|
||||||
let value = state
|
};
|
||||||
.storage(next_key.as_ref())
|
let mut current_child = if start_key.len() == 2 {
|
||||||
|
let start_key = start_key.get(0).expect("checked len");
|
||||||
|
if let Some(child_root) = state
|
||||||
|
.storage(&start_key)
|
||||||
.map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?
|
.map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?
|
||||||
.unwrap_or_default();
|
{
|
||||||
let size = value.len() + next_key.len();
|
Some((child_info(start_key)?, child_root))
|
||||||
if total_size + size > size_limit && !entries.is_empty() {
|
} else {
|
||||||
|
return Err(Error::Backend("Invalid root start key.".to_string()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let mut current_key = start_key.last().map(Clone::clone).unwrap_or(Vec::new());
|
||||||
|
let mut total_size = 0;
|
||||||
|
let mut result = vec![(
|
||||||
|
KeyValueStorageLevel {
|
||||||
|
state_root: Vec::new(),
|
||||||
|
key_values: Vec::new(),
|
||||||
|
parent_storage_keys: Vec::new(),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)];
|
||||||
|
|
||||||
|
let mut child_roots = HashSet::new();
|
||||||
|
loop {
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
let mut complete = true;
|
||||||
|
let mut switch_child_key = None;
|
||||||
|
while let Some(next_key) = if let Some(child) = current_child.as_ref() {
|
||||||
|
state
|
||||||
|
.next_child_storage_key(&child.0, ¤t_key)
|
||||||
|
.map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?
|
||||||
|
} else {
|
||||||
|
state
|
||||||
|
.next_storage_key(¤t_key)
|
||||||
|
.map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?
|
||||||
|
} {
|
||||||
|
let value = if let Some(child) = current_child.as_ref() {
|
||||||
|
state
|
||||||
|
.child_storage(&child.0, next_key.as_ref())
|
||||||
|
.map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
state
|
||||||
|
.storage(next_key.as_ref())
|
||||||
|
.map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
let size = value.len() + next_key.len();
|
||||||
|
if total_size + size > size_limit && !entries.is_empty() {
|
||||||
|
complete = false;
|
||||||
|
break
|
||||||
|
}
|
||||||
|
total_size += size;
|
||||||
|
|
||||||
|
if current_child.is_none() &&
|
||||||
|
sp_core::storage::well_known_keys::is_child_storage_key(next_key.as_slice())
|
||||||
|
{
|
||||||
|
if !child_roots.contains(value.as_slice()) {
|
||||||
|
child_roots.insert(value.clone());
|
||||||
|
switch_child_key = Some((next_key.clone(), value.clone()));
|
||||||
|
entries.push((next_key.clone(), value));
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.push((next_key.clone(), value));
|
||||||
|
current_key = next_key;
|
||||||
|
}
|
||||||
|
if let Some((child, child_root)) = switch_child_key.take() {
|
||||||
|
result[0].0.key_values.extend(entries.into_iter());
|
||||||
|
current_child = Some((child_info(&child)?, child_root));
|
||||||
|
current_key = Vec::new();
|
||||||
|
} else if let Some((child, child_root)) = current_child.take() {
|
||||||
|
current_key = child.into_prefixed_storage_key().into_inner();
|
||||||
|
result.push((
|
||||||
|
KeyValueStorageLevel {
|
||||||
|
state_root: child_root,
|
||||||
|
key_values: entries,
|
||||||
|
parent_storage_keys: Vec::new(),
|
||||||
|
},
|
||||||
|
complete,
|
||||||
|
));
|
||||||
|
if !complete {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result[0].0.key_values.extend(entries.into_iter());
|
||||||
|
result[0].1 = complete;
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
total_size += size;
|
|
||||||
entries.push((next_key.clone(), value));
|
|
||||||
current_key = next_key;
|
|
||||||
}
|
}
|
||||||
Ok(entries)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_range_proof(
|
fn verify_range_proof(
|
||||||
&self,
|
&self,
|
||||||
root: Block::Hash,
|
root: Block::Hash,
|
||||||
proof: StorageProof,
|
proof: CompactProof,
|
||||||
start_key: &[u8],
|
start_key: &[Vec<u8>],
|
||||||
) -> sp_blockchain::Result<(Vec<(Vec<u8>, Vec<u8>)>, bool)> {
|
) -> sp_blockchain::Result<(KeyValueStates, usize)> {
|
||||||
Ok(read_range_proof_check::<HashFor<Block>>(
|
let mut db = sp_state_machine::MemoryDB::<HashFor<Block>>::new(&[]);
|
||||||
root,
|
let _ = sp_trie::decode_compact::<sp_state_machine::Layout<HashFor<Block>>, _, _>(
|
||||||
proof,
|
&mut db,
|
||||||
None,
|
proof.iter_compact_encoded_nodes(),
|
||||||
None,
|
Some(&root),
|
||||||
None,
|
)
|
||||||
Some(start_key),
|
.map_err(|e| sp_blockchain::Error::from_state(Box::new(e)))?;
|
||||||
)?)
|
let proving_backend = sp_state_machine::TrieBackend::new(db, root);
|
||||||
|
let state = read_range_proof_check_with_child_on_proving_backend::<HashFor<Block>>(
|
||||||
|
&proving_backend,
|
||||||
|
start_key,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ mod std_reexport {
|
|||||||
};
|
};
|
||||||
pub use sp_trie::{
|
pub use sp_trie::{
|
||||||
trie_types::{Layout, TrieDBMut},
|
trie_types::{Layout, TrieDBMut},
|
||||||
DBValue, MemoryDB, StorageProof, TrieMut,
|
CompactProof, DBValue, MemoryDB, StorageProof, TrieMut,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,15 +181,20 @@ mod execution {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use codec::{Codec, Decode, Encode};
|
use codec::{Codec, Decode, Encode};
|
||||||
use hash_db::Hasher;
|
use hash_db::Hasher;
|
||||||
|
use smallvec::SmallVec;
|
||||||
use sp_core::{
|
use sp_core::{
|
||||||
hexdisplay::HexDisplay,
|
hexdisplay::HexDisplay,
|
||||||
storage::ChildInfo,
|
storage::{ChildInfo, ChildType, PrefixedStorageKey},
|
||||||
traits::{CodeExecutor, ReadRuntimeVersionExt, RuntimeCode, SpawnNamed},
|
traits::{CodeExecutor, ReadRuntimeVersionExt, RuntimeCode, SpawnNamed},
|
||||||
NativeOrEncoded, NeverNativeValue,
|
NativeOrEncoded, NeverNativeValue,
|
||||||
};
|
};
|
||||||
use sp_externalities::Extensions;
|
use sp_externalities::Extensions;
|
||||||
use std::{collections::HashMap, fmt, panic::UnwindSafe, result};
|
use std::{
|
||||||
use tracing::{trace, warn};
|
collections::{HashMap, HashSet},
|
||||||
|
fmt,
|
||||||
|
panic::UnwindSafe,
|
||||||
|
result,
|
||||||
|
};
|
||||||
|
|
||||||
const PROOF_CLOSE_TRANSACTION: &str = "\
|
const PROOF_CLOSE_TRANSACTION: &str = "\
|
||||||
Closing a transaction that was started in this function. Client initiated transactions
|
Closing a transaction that was started in this function. Client initiated transactions
|
||||||
@@ -742,6 +747,254 @@ mod execution {
|
|||||||
prove_read_on_trie_backend(trie_backend, keys)
|
prove_read_on_trie_backend(trie_backend, keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// State machine only allows a single level
|
||||||
|
/// of child trie.
|
||||||
|
pub const MAX_NESTED_TRIE_DEPTH: usize = 2;
|
||||||
|
|
||||||
|
/// Multiple key value state.
|
||||||
|
/// States are ordered by root storage key.
|
||||||
|
#[derive(PartialEq, Eq, Clone)]
|
||||||
|
pub struct KeyValueStates(pub Vec<KeyValueStorageLevel>);
|
||||||
|
|
||||||
|
/// A key value state at any storage level.
|
||||||
|
#[derive(PartialEq, Eq, Clone)]
|
||||||
|
pub struct KeyValueStorageLevel {
|
||||||
|
/// State root of the level, for
|
||||||
|
/// top trie it is as an empty byte array.
|
||||||
|
pub state_root: Vec<u8>,
|
||||||
|
/// Storage of parents, empty for top root or
|
||||||
|
/// when exporting (building proof).
|
||||||
|
pub parent_storage_keys: Vec<Vec<u8>>,
|
||||||
|
/// Pair of key and values from this state.
|
||||||
|
pub key_values: Vec<(Vec<u8>, Vec<u8>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I> From<I> for KeyValueStates
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = (Vec<u8>, (Vec<(Vec<u8>, Vec<u8>)>, Vec<Vec<u8>>))>,
|
||||||
|
{
|
||||||
|
fn from(b: I) -> Self {
|
||||||
|
let mut result = Vec::new();
|
||||||
|
for (state_root, (key_values, storage_paths)) in b.into_iter() {
|
||||||
|
result.push(KeyValueStorageLevel {
|
||||||
|
state_root,
|
||||||
|
key_values,
|
||||||
|
parent_storage_keys: storage_paths,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
KeyValueStates(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyValueStates {
|
||||||
|
/// Return total number of key values in states.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.0.iter().fold(0, |nb, state| nb + state.key_values.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update last keys accessed from this state.
|
||||||
|
pub fn update_last_key(
|
||||||
|
&self,
|
||||||
|
stopped_at: usize,
|
||||||
|
last: &mut SmallVec<[Vec<u8>; 2]>,
|
||||||
|
) -> bool {
|
||||||
|
if stopped_at == 0 || stopped_at > MAX_NESTED_TRIE_DEPTH {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
match stopped_at {
|
||||||
|
1 => {
|
||||||
|
let top_last =
|
||||||
|
self.0.get(0).and_then(|s| s.key_values.last().map(|kv| kv.0.clone()));
|
||||||
|
if let Some(top_last) = top_last {
|
||||||
|
match last.len() {
|
||||||
|
0 => {
|
||||||
|
last.push(top_last);
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
2 => {
|
||||||
|
last.pop();
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
// update top trie access.
|
||||||
|
last[0] = top_last;
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// No change in top trie accesses.
|
||||||
|
// Indicates end of reading of a child trie.
|
||||||
|
last.truncate(1);
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
2 => {
|
||||||
|
let top_last =
|
||||||
|
self.0.get(0).and_then(|s| s.key_values.last().map(|kv| kv.0.clone()));
|
||||||
|
let child_last =
|
||||||
|
self.0.last().and_then(|s| s.key_values.last().map(|kv| kv.0.clone()));
|
||||||
|
|
||||||
|
if let Some(child_last) = child_last {
|
||||||
|
if last.len() == 0 {
|
||||||
|
if let Some(top_last) = top_last {
|
||||||
|
last.push(top_last)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else if let Some(top_last) = top_last {
|
||||||
|
last[0] = top_last;
|
||||||
|
}
|
||||||
|
if last.len() == 2 {
|
||||||
|
last.pop();
|
||||||
|
}
|
||||||
|
last.push(child_last);
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
// stopped at level 2 so child last is define.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate range storage read proof, with child tries
|
||||||
|
/// content.
|
||||||
|
/// A size limit is applied to the proof with the
|
||||||
|
/// exception that `start_at` and its following element
|
||||||
|
/// are always part of the proof.
|
||||||
|
/// If a key different than `start_at` is a child trie root,
|
||||||
|
/// the child trie content will be included in the proof.
|
||||||
|
pub fn prove_range_read_with_child_with_size<B, H>(
|
||||||
|
backend: B,
|
||||||
|
size_limit: usize,
|
||||||
|
start_at: &[Vec<u8>],
|
||||||
|
) -> Result<(StorageProof, u32), Box<dyn Error>>
|
||||||
|
where
|
||||||
|
B: Backend<H>,
|
||||||
|
H: Hasher,
|
||||||
|
H::Out: Ord + Codec,
|
||||||
|
{
|
||||||
|
let trie_backend = backend
|
||||||
|
.as_trie_backend()
|
||||||
|
.ok_or_else(|| Box::new(ExecutionError::UnableToGenerateProof) as Box<dyn Error>)?;
|
||||||
|
prove_range_read_with_child_with_size_on_trie_backend(trie_backend, size_limit, start_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate range storage read proof, with child tries
|
||||||
|
/// content.
|
||||||
|
/// See `prove_range_read_with_child_with_size`.
|
||||||
|
pub fn prove_range_read_with_child_with_size_on_trie_backend<S, H>(
|
||||||
|
trie_backend: &TrieBackend<S, H>,
|
||||||
|
size_limit: usize,
|
||||||
|
start_at: &[Vec<u8>],
|
||||||
|
) -> Result<(StorageProof, u32), Box<dyn Error>>
|
||||||
|
where
|
||||||
|
S: trie_backend_essence::TrieBackendStorage<H>,
|
||||||
|
H: Hasher,
|
||||||
|
H::Out: Ord + Codec,
|
||||||
|
{
|
||||||
|
if start_at.len() > MAX_NESTED_TRIE_DEPTH {
|
||||||
|
return Err(Box::new("Invalid start of range."))
|
||||||
|
}
|
||||||
|
|
||||||
|
let proving_backend = proving_backend::ProvingBackend::<S, H>::new(trie_backend);
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
let mut child_roots = HashSet::new();
|
||||||
|
let (mut child_key, mut start_at) = if start_at.len() == 2 {
|
||||||
|
let storage_key = start_at.get(0).expect("Checked length.").clone();
|
||||||
|
if let Some(state_root) = proving_backend
|
||||||
|
.storage(&storage_key)
|
||||||
|
.map_err(|e| Box::new(e) as Box<dyn Error>)?
|
||||||
|
{
|
||||||
|
child_roots.insert(state_root.clone());
|
||||||
|
} else {
|
||||||
|
return Err(Box::new("Invalid range start child trie key."))
|
||||||
|
}
|
||||||
|
|
||||||
|
(Some(storage_key), start_at.get(1).cloned())
|
||||||
|
} else {
|
||||||
|
(None, start_at.get(0).cloned())
|
||||||
|
};
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (child_info, depth) = if let Some(storage_key) = child_key.as_ref() {
|
||||||
|
let storage_key = PrefixedStorageKey::new_ref(storage_key);
|
||||||
|
(
|
||||||
|
Some(match ChildType::from_prefixed_key(&storage_key) {
|
||||||
|
Some((ChildType::ParentKeyId, storage_key)) =>
|
||||||
|
ChildInfo::new_default(storage_key),
|
||||||
|
None => return Err(Box::new("Invalid range start child trie key.")),
|
||||||
|
}),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(None, 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
let start_at_ref = start_at.as_ref().map(AsRef::as_ref);
|
||||||
|
let mut switch_child_key = None;
|
||||||
|
let mut first = start_at.is_some();
|
||||||
|
let completed = proving_backend
|
||||||
|
.apply_to_key_values_while(
|
||||||
|
child_info.as_ref(),
|
||||||
|
None,
|
||||||
|
start_at_ref,
|
||||||
|
|key, value| {
|
||||||
|
if first {
|
||||||
|
if start_at_ref
|
||||||
|
.as_ref()
|
||||||
|
.map(|start| &key.as_slice() > start)
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if first {
|
||||||
|
true
|
||||||
|
} else if depth < MAX_NESTED_TRIE_DEPTH &&
|
||||||
|
sp_core::storage::well_known_keys::is_child_storage_key(
|
||||||
|
key.as_slice(),
|
||||||
|
) {
|
||||||
|
count += 1;
|
||||||
|
if !child_roots.contains(value.as_slice()) {
|
||||||
|
child_roots.insert(value);
|
||||||
|
switch_child_key = Some(key);
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
// do not add two child trie with same root
|
||||||
|
true
|
||||||
|
}
|
||||||
|
} else if proving_backend.estimate_encoded_size() <= size_limit {
|
||||||
|
count += 1;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.map_err(|e| Box::new(e) as Box<dyn Error>)?;
|
||||||
|
|
||||||
|
if switch_child_key.is_none() {
|
||||||
|
if depth == 1 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
if completed {
|
||||||
|
start_at = child_key.take();
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
child_key = switch_child_key;
|
||||||
|
start_at = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((proving_backend.extract_proof(), count))
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate range storage read proof.
|
/// Generate range storage read proof.
|
||||||
pub fn prove_range_read_with_size<B, H>(
|
pub fn prove_range_read_with_size<B, H>(
|
||||||
backend: B,
|
backend: B,
|
||||||
@@ -884,7 +1137,25 @@ mod execution {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check child storage range proof, generated by `prove_range_read` call.
|
/// Check storage range proof with child trie included, generated by
|
||||||
|
/// `prove_range_read_with_child_with_size` call.
|
||||||
|
///
|
||||||
|
/// Returns key values contents and the depth of the pending state iteration
|
||||||
|
/// (0 if completed).
|
||||||
|
pub fn read_range_proof_check_with_child<H>(
|
||||||
|
root: H::Out,
|
||||||
|
proof: StorageProof,
|
||||||
|
start_at: &[Vec<u8>],
|
||||||
|
) -> Result<(KeyValueStates, usize), Box<dyn Error>>
|
||||||
|
where
|
||||||
|
H: Hasher,
|
||||||
|
H::Out: Ord + Codec,
|
||||||
|
{
|
||||||
|
let proving_backend = create_proof_check_backend::<H>(root, proof)?;
|
||||||
|
read_range_proof_check_with_child_on_proving_backend(&proving_backend, start_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check child storage range proof, generated by `prove_range_read_with_size` call.
|
||||||
pub fn read_range_proof_check<H>(
|
pub fn read_range_proof_check<H>(
|
||||||
root: H::Out,
|
root: H::Out,
|
||||||
proof: StorageProof,
|
proof: StorageProof,
|
||||||
@@ -991,6 +1262,130 @@ mod execution {
|
|||||||
Err(e) => Err(Box::new(e) as Box<dyn Error>),
|
Err(e) => Err(Box::new(e) as Box<dyn Error>),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check storage range proof on pre-created proving backend.
|
||||||
|
///
|
||||||
|
/// See `read_range_proof_check_with_child`.
|
||||||
|
pub fn read_range_proof_check_with_child_on_proving_backend<H>(
|
||||||
|
proving_backend: &TrieBackend<MemoryDB<H>, H>,
|
||||||
|
start_at: &[Vec<u8>],
|
||||||
|
) -> Result<(KeyValueStates, usize), Box<dyn Error>>
|
||||||
|
where
|
||||||
|
H: Hasher,
|
||||||
|
H::Out: Ord + Codec,
|
||||||
|
{
|
||||||
|
let mut result = vec![KeyValueStorageLevel {
|
||||||
|
state_root: Default::default(),
|
||||||
|
key_values: Default::default(),
|
||||||
|
parent_storage_keys: Default::default(),
|
||||||
|
}];
|
||||||
|
if start_at.len() > MAX_NESTED_TRIE_DEPTH {
|
||||||
|
return Err(Box::new("Invalid start of range."))
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut child_roots = HashSet::new();
|
||||||
|
let (mut child_key, mut start_at) = if start_at.len() == 2 {
|
||||||
|
let storage_key = start_at.get(0).expect("Checked length.").clone();
|
||||||
|
let child_key = if let Some(state_root) = proving_backend
|
||||||
|
.storage(&storage_key)
|
||||||
|
.map_err(|e| Box::new(e) as Box<dyn Error>)?
|
||||||
|
{
|
||||||
|
child_roots.insert(state_root.clone());
|
||||||
|
Some((storage_key, state_root))
|
||||||
|
} else {
|
||||||
|
return Err(Box::new("Invalid range start child trie key."))
|
||||||
|
};
|
||||||
|
|
||||||
|
(child_key, start_at.get(1).cloned())
|
||||||
|
} else {
|
||||||
|
(None, start_at.get(0).cloned())
|
||||||
|
};
|
||||||
|
|
||||||
|
let completed = loop {
|
||||||
|
let (child_info, depth) = if let Some((storage_key, state_root)) = child_key.as_ref() {
|
||||||
|
result.push(KeyValueStorageLevel {
|
||||||
|
state_root: state_root.clone(),
|
||||||
|
key_values: Default::default(),
|
||||||
|
parent_storage_keys: Default::default(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let storage_key = PrefixedStorageKey::new_ref(storage_key);
|
||||||
|
(
|
||||||
|
Some(match ChildType::from_prefixed_key(&storage_key) {
|
||||||
|
Some((ChildType::ParentKeyId, storage_key)) =>
|
||||||
|
ChildInfo::new_default(storage_key),
|
||||||
|
None => return Err(Box::new("Invalid range start child trie key.")),
|
||||||
|
}),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(None, 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
let values = if child_info.is_some() {
|
||||||
|
&mut result.last_mut().expect("Added above").key_values
|
||||||
|
} else {
|
||||||
|
&mut result[0].key_values
|
||||||
|
};
|
||||||
|
let start_at_ref = start_at.as_ref().map(AsRef::as_ref);
|
||||||
|
let mut switch_child_key = None;
|
||||||
|
let mut first = start_at.is_some();
|
||||||
|
let completed = proving_backend
|
||||||
|
.apply_to_key_values_while(
|
||||||
|
child_info.as_ref(),
|
||||||
|
None,
|
||||||
|
start_at_ref,
|
||||||
|
|key, value| {
|
||||||
|
if first {
|
||||||
|
if start_at_ref
|
||||||
|
.as_ref()
|
||||||
|
.map(|start| &key.as_slice() > start)
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !first {
|
||||||
|
values.push((key.to_vec(), value.to_vec()));
|
||||||
|
}
|
||||||
|
if first {
|
||||||
|
true
|
||||||
|
} else if depth < MAX_NESTED_TRIE_DEPTH &&
|
||||||
|
sp_core::storage::well_known_keys::is_child_storage_key(
|
||||||
|
key.as_slice(),
|
||||||
|
) {
|
||||||
|
if child_roots.contains(value.as_slice()) {
|
||||||
|
// Do not add two chid trie with same root.
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
child_roots.insert(value.clone());
|
||||||
|
switch_child_key = Some((key, value));
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.map_err(|e| Box::new(e) as Box<dyn Error>)?;
|
||||||
|
|
||||||
|
if switch_child_key.is_none() {
|
||||||
|
if !completed {
|
||||||
|
break depth
|
||||||
|
}
|
||||||
|
if depth == 1 {
|
||||||
|
break 0
|
||||||
|
} else {
|
||||||
|
start_at = child_key.take().map(|entry| entry.0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
child_key = switch_child_key;
|
||||||
|
start_at = None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok((KeyValueStates(result), completed))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -1574,7 +1969,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
local_result1.into_iter().collect::<Vec<_>>(),
|
local_result1.into_iter().collect::<Vec<_>>(),
|
||||||
vec![(b"value3".to_vec(), Some(vec![142]))],
|
vec![(b"value3".to_vec(), Some(vec![142; 33]))],
|
||||||
);
|
);
|
||||||
assert_eq!(local_result2.into_iter().collect::<Vec<_>>(), vec![(b"value2".to_vec(), None)]);
|
assert_eq!(local_result2.into_iter().collect::<Vec<_>>(), vec![(b"value2".to_vec(), None)]);
|
||||||
assert_eq!(local_result3.into_iter().collect::<Vec<_>>(), vec![(b"dummy".to_vec(), None)]);
|
assert_eq!(local_result3.into_iter().collect::<Vec<_>>(), vec![(b"dummy".to_vec(), None)]);
|
||||||
@@ -1678,7 +2073,7 @@ mod tests {
|
|||||||
let remote_root = remote_backend.storage_root(::std::iter::empty()).0;
|
let remote_root = remote_backend.storage_root(::std::iter::empty()).0;
|
||||||
let (proof, count) =
|
let (proof, count) =
|
||||||
prove_range_read_with_size(remote_backend, None, None, 0, None).unwrap();
|
prove_range_read_with_size(remote_backend, None, None, 0, None).unwrap();
|
||||||
// Alwasys contains at least some nodes.
|
// Always contains at least some nodes.
|
||||||
assert_eq!(proof.into_memory_db::<BlakeTwo256>().drain().len(), 3);
|
assert_eq!(proof.into_memory_db::<BlakeTwo256>().drain().len(), 3);
|
||||||
assert_eq!(count, 1);
|
assert_eq!(count, 1);
|
||||||
|
|
||||||
@@ -1723,6 +2118,45 @@ mod tests {
|
|||||||
assert_eq!(completed, true);
|
assert_eq!(completed, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prove_range_with_child_works() {
|
||||||
|
let remote_backend = trie_backend::tests::test_trie();
|
||||||
|
let remote_root = remote_backend.storage_root(::std::iter::empty()).0;
|
||||||
|
let mut start_at = smallvec::SmallVec::<[Vec<u8>; 2]>::new();
|
||||||
|
let trie_backend = remote_backend.as_trie_backend().unwrap();
|
||||||
|
let max_iter = 1000;
|
||||||
|
let mut nb_loop = 0;
|
||||||
|
loop {
|
||||||
|
nb_loop += 1;
|
||||||
|
if max_iter == nb_loop {
|
||||||
|
panic!("Too many loop in prove range");
|
||||||
|
}
|
||||||
|
let (proof, count) = prove_range_read_with_child_with_size_on_trie_backend(
|
||||||
|
trie_backend,
|
||||||
|
1,
|
||||||
|
start_at.as_slice(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
// Always contains at least some nodes.
|
||||||
|
assert!(proof.clone().into_memory_db::<BlakeTwo256>().drain().len() > 0);
|
||||||
|
assert!(count < 3); // when doing child we include parent and first child key.
|
||||||
|
|
||||||
|
let (result, completed_depth) = read_range_proof_check_with_child::<BlakeTwo256>(
|
||||||
|
remote_root,
|
||||||
|
proof.clone(),
|
||||||
|
start_at.as_slice(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if completed_depth == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
assert!(result.update_last_key(completed_depth, &mut start_at));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(nb_loop, 10);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn compact_multiple_child_trie() {
|
fn compact_multiple_child_trie() {
|
||||||
// this root will be queried
|
// this root will be queried
|
||||||
|
|||||||
@@ -281,8 +281,8 @@ pub mod tests {
|
|||||||
{
|
{
|
||||||
let mut mdb = KeySpacedDBMut::new(&mut mdb, child_info.keyspace());
|
let mut mdb = KeySpacedDBMut::new(&mut mdb, child_info.keyspace());
|
||||||
let mut trie = TrieDBMut::new(&mut mdb, &mut root);
|
let mut trie = TrieDBMut::new(&mut mdb, &mut root);
|
||||||
trie.insert(b"value3", &[142]).expect("insert failed");
|
trie.insert(b"value3", &[142; 33]).expect("insert failed");
|
||||||
trie.insert(b"value4", &[124]).expect("insert failed");
|
trie.insert(b"value4", &[124; 33]).expect("insert failed");
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -319,7 +319,7 @@ pub mod tests {
|
|||||||
test_trie
|
test_trie
|
||||||
.child_storage(&ChildInfo::new_default(CHILD_KEY_1), b"value3")
|
.child_storage(&ChildInfo::new_default(CHILD_KEY_1), b"value3")
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
Some(vec![142u8]),
|
Some(vec![142u8; 33]),
|
||||||
);
|
);
|
||||||
// Change cache entry to check that caching is active.
|
// Change cache entry to check that caching is active.
|
||||||
test_trie
|
test_trie
|
||||||
|
|||||||
@@ -137,6 +137,11 @@ impl GenesisParameters {
|
|||||||
pub fn set_wasm_code(&mut self, code: Vec<u8>) {
|
pub fn set_wasm_code(&mut self, code: Vec<u8>) {
|
||||||
self.wasm_code = Some(code);
|
self.wasm_code = Some(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Access extra genesis storage.
|
||||||
|
pub fn extra_storage(&mut self) -> &mut Storage {
|
||||||
|
&mut self.extra_storage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl substrate_test_client::GenesisInit for GenesisParameters {
|
impl substrate_test_client::GenesisInit for GenesisParameters {
|
||||||
|
|||||||
Reference in New Issue
Block a user