diff --git a/substrate/substrate/primitives/src/storage.rs b/substrate/substrate/primitives/src/storage.rs index 25bb11fb6e549ea7b87778bf8e56798a364f5f64..a3330571fcec6391164ed47d2d0132ae718ad535 100644 --- a/substrate/substrate/primitives/src/storage.rs +++ b/substrate/substrate/primitives/src/storage.rs @@ -31,7 +31,7 @@ pub struct StorageKey(#[cfg_attr(feature = "std", serde(with="bytes"))] pub Vec< pub struct StorageData(#[cfg_attr(feature = "std", serde(with="bytes"))] pub Vec<u8>); /// Storage change set -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize, Debug, PartialEq, Eq))] pub struct StorageChangeSet<Hash> { /// Block hash pub block: Hash, diff --git a/substrate/substrate/rpc/src/state/error.rs b/substrate/substrate/rpc/src/state/error.rs index 24adeb29d386af20f9b7ae8e475c578f81ca48a0..587937ea607ab38f6f6d6184c699642eba47f11d 100644 --- a/substrate/substrate/rpc/src/state/error.rs +++ b/substrate/substrate/rpc/src/state/error.rs @@ -25,6 +25,11 @@ error_chain! { } errors { + /// Provided block range couldn't be resolved to a list of blocks. + InvalidBlockRange(from: String, to: String, details: String) { + description("Invalid block range"), + display("Cannot resolve a block range ['{:?}' ... '{:?}]. {}", from, to, details), + } /// Not implemented yet Unimplemented { description("not implemented yet"), diff --git a/substrate/substrate/rpc/src/state/mod.rs b/substrate/substrate/rpc/src/state/mod.rs index 25f985c206cdca41210c474ae6564032722a3011..0ae462452fe1c7adc791fbe8ac4f095b348b3f14 100644 --- a/substrate/substrate/rpc/src/state/mod.rs +++ b/substrate/substrate/rpc/src/state/mod.rs @@ -16,7 +16,10 @@ //! Polkadot state API. -use std::sync::Arc; +use std::{ + collections::HashMap, + sync::Arc, +}; use client::{self, Client, CallExecutor, BlockchainEvents}; use jsonrpc_macros::Trailing; @@ -27,7 +30,7 @@ use primitives::storage::{StorageKey, StorageData, StorageChangeSet}; use rpc::Result as RpcResult; use rpc::futures::{stream, Future, Sink, Stream}; use runtime_primitives::generic::BlockId; -use runtime_primitives::traits::Block as BlockT; +use runtime_primitives::traits::{Block as BlockT, Header}; use tokio::runtime::TaskExecutor; use subscriptions::Subscriptions; @@ -59,6 +62,13 @@ build_rpc_trait! { #[rpc(name = "state_getStorageSize", alias = ["state_getStorageSizeAt", ])] fn storage_size(&self, StorageKey, Trailing<Hash>) -> Result<Option<u64>>; + /// Query historical storage entries (by key) starting from a block given as the second parameter. + /// + /// NOTE This first returned result contains the initial state of storage for all keys. + /// Subsequent values in the vector represent changes to the previous state (diffs). + #[rpc(name = "state_queryStorage")] + fn query_storage(&self, Vec<StorageKey>, Hash, Trailing<Hash>) -> Result<Vec<StorageChangeSet<Hash>>>; + #[pubsub(name = "state_storage")] { /// New storage subscription #[rpc(name = "state_subscribeStorage")] @@ -130,6 +140,74 @@ impl<B, E, Block> StateApi<Block::Hash> for State<B, E, Block> where Ok(self.storage(key, block)?.map(|x| x.0.len() as u64)) } + fn query_storage(&self, keys: Vec<StorageKey>, from: Block::Hash, to: Trailing<Block::Hash>) -> Result<Vec<StorageChangeSet<Block::Hash>>> { + let to = self.unwrap_or_best(to)?; + + let from_hdr = self.client.header(&BlockId::hash(from))?; + let to_hdr = self.client.header(&BlockId::hash(to))?; + + match (from_hdr, to_hdr) { + (Some(ref from), Some(ref to)) if from.number() <= to.number() => { + let from = from.clone(); + let to = to.clone(); + // check if we can get from `to` to `from` by going through parent_hashes. + let blocks = { + let mut blocks = vec![to.hash()]; + let mut last = to.clone(); + while last.number() > from.number() { + if let Some(hdr) = self.client.header(&BlockId::hash(*last.parent_hash()))? { + blocks.push(hdr.hash()); + last = hdr; + } else { + bail!(invalid_block_range( + Some(from), + Some(to), + format!("Parent of {} ({}) not found", last.number(), last.hash()), + )) + } + } + if last.hash() != from.hash() { + bail!(invalid_block_range( + Some(from), + Some(to), + format!("Expected to reach `from`, got {} ({})", last.number(), last.hash()), + )) + } + blocks.reverse(); + blocks + }; + let mut result = Vec::new(); + let mut last_state: HashMap<_, Option<_>> = Default::default(); + for block in blocks { + let mut changes = vec![]; + let id = BlockId::hash(block.clone()); + + for key in &keys { + let (has_changed, data) = { + let curr_data = self.client.storage(&id, key)?; + let prev_data = last_state.get(key).and_then(|x| x.as_ref()); + + (curr_data.as_ref() != prev_data, curr_data) + }; + + if has_changed { + changes.push((key.clone(), data.clone())); + } + + last_state.insert(key.clone(), data); + } + + result.push(StorageChangeSet { + block, + changes, + }); + } + Ok(result) + }, + (from, to) => bail!(invalid_block_range(from, to, "Invalid range or unknown block".into())), + } + } + fn subscribe_storage( &self, _meta: Self::Metadata, @@ -179,3 +257,12 @@ impl<B, E, Block> StateApi<Block::Hash> for State<B, E, Block> where Ok(self.subscriptions.cancel(id)) } } + +fn invalid_block_range<H: Header>(from: Option<H>, to: Option<H>, reason: String) -> error::ErrorKind { + let to_string = |x: Option<H>| match x { + None => "unknown hash".into(), + Some(h) => format!("{} ({})", h.number(), h.hash()), + }; + + error::ErrorKind::InvalidBlockRange(to_string(from), to_string(to), reason) +} diff --git a/substrate/substrate/rpc/src/state/tests.rs b/substrate/substrate/rpc/src/state/tests.rs index 1ab9d1896a1d927165824f8ec2f5e53276b0c306..bad934b8405a30d095d52ddd3d9f15d5bf679dee 100644 --- a/substrate/substrate/rpc/src/state/tests.rs +++ b/substrate/substrate/rpc/src/state/tests.rs @@ -22,7 +22,6 @@ use jsonrpc_macros::pubsub; use rustc_hex::FromHex; use test_client::{self, runtime, keyring::Keyring, TestClient, BlockBuilderExt}; - #[test] fn should_return_storage() { let core = ::tokio::runtime::Runtime::new().unwrap(); @@ -121,3 +120,67 @@ fn should_send_initial_storage_changes_and_notifications() { // no more notifications on this channel assert_eq!(core.block_on(next.into_future()).unwrap().0, None); } + +#[test] +fn should_query_storage() { + let core = ::tokio::runtime::Runtime::new().unwrap(); + let client = Arc::new(test_client::new()); + let api = State::new(client.clone(), core.executor()); + + let add_block = |nonce| { + let mut builder = client.new_block().unwrap(); + builder.push_transfer(runtime::Transfer { + from: Keyring::Alice.to_raw_public().into(), + to: Keyring::Ferdie.to_raw_public().into(), + amount: 42, + nonce, + }).unwrap(); + let block = builder.bake().unwrap(); + let hash = block.header.hash(); + client.justify_and_import(BlockOrigin::Own, block).unwrap(); + hash + }; + let block1_hash = add_block(0); + let block2_hash = add_block(1); + let genesis_hash = client.genesis_hash(); + + + let mut expected = vec![ + StorageChangeSet { + block: genesis_hash, + changes: vec![ + (StorageKey("a52da2b7c269da1366b3ed1cdb7299ce".from_hex().unwrap()), Some(StorageData(vec![232, 3, 0, 0, 0, 0, 0, 0]))), + ], + }, + StorageChangeSet { + block: block1_hash, + changes: vec![ + (StorageKey("a52da2b7c269da1366b3ed1cdb7299ce".from_hex().unwrap()), Some(StorageData(vec![190, 3, 0, 0, 0, 0, 0, 0]))), + ], + }, + ]; + + // Query changes only up to block1 + let result = api.query_storage( + vec![StorageKey("a52da2b7c269da1366b3ed1cdb7299ce".from_hex().unwrap())], + genesis_hash, + Some(block1_hash).into(), + ); + + assert_eq!(result.unwrap(), expected); + + // Query all changes + let result = api.query_storage( + vec![StorageKey("a52da2b7c269da1366b3ed1cdb7299ce".from_hex().unwrap())], + genesis_hash, + None.into(), + ); + + expected.push(StorageChangeSet { + block: block2_hash, + changes: vec![ + (StorageKey("a52da2b7c269da1366b3ed1cdb7299ce".from_hex().unwrap()), Some(StorageData(vec![148, 3, 0, 0, 0, 0, 0, 0]))), + ], + }); + assert_eq!(result.unwrap(), expected); +}