diff --git a/substrate/client/rpc-api/src/state/mod.rs b/substrate/client/rpc-api/src/state/mod.rs
index b2cf8ce909b20fead05a3be7bdb9d0d93d6fade1..d29e46a4b5637cf85d0745d59a73b0ad69afde7c 100644
--- a/substrate/client/rpc-api/src/state/mod.rs
+++ b/substrate/client/rpc-api/src/state/mod.rs
@@ -136,6 +136,14 @@ pub trait StateApi<Hash> {
 		hash: Option<Hash>
 	) -> FutureResult<Vec<StorageChangeSet<Hash>>>;
 
+	/// Query storage entries (by key) starting at block hash given as the second parameter.
+	#[rpc(name = "state_queryStorageAt")]
+	fn query_storage_at(
+		&self,
+		keys: Vec<StorageKey>,
+		at: Option<Hash>,
+	) -> FutureResult<Vec<StorageChangeSet<Hash>>>;
+
 	/// New runtime version subscription
 	#[pubsub(
 		subscription = "state_runtimeVersion",
diff --git a/substrate/client/rpc/src/state/mod.rs b/substrate/client/rpc/src/state/mod.rs
index 82568866ee3ba4df6686d9201783f7cf8cfa673e..2747405c04fcac0c7534ea802bb5e1f08a7e1911 100644
--- a/substrate/client/rpc/src/state/mod.rs
+++ b/substrate/client/rpc/src/state/mod.rs
@@ -163,6 +163,13 @@ pub trait StateBackend<Block: BlockT, Client>: Send + Sync + 'static
 		keys: Vec<StorageKey>,
 	) -> FutureResult<Vec<StorageChangeSet<Block::Hash>>>;
 
+	/// Query storage entries (by key) starting at block hash given as the second parameter.
+	fn query_storage_at(
+		&self,
+		keys: Vec<StorageKey>,
+		at: Option<Block::Hash>
+	) -> FutureResult<Vec<StorageChangeSet<Block::Hash>>>;
+
 	/// New runtime version subscription
 	fn subscribe_runtime_version(
 		&self,
@@ -357,6 +364,14 @@ impl<Block, Client> StateApi<Block::Hash> for State<Block, Client>
 		self.backend.query_storage(from, to, keys)
 	}
 
+	fn query_storage_at(
+		&self,
+		keys: Vec<StorageKey>,
+		at: Option<Block::Hash>
+	) -> FutureResult<Vec<StorageChangeSet<Block::Hash>>> {
+		self.backend.query_storage_at(keys, at)
+	}
+
 	fn subscribe_storage(
 		&self,
 		meta: Self::Metadata,
diff --git a/substrate/client/rpc/src/state/state_full.rs b/substrate/client/rpc/src/state/state_full.rs
index b7589d2aefecafc9daa41e2122461c21f53c93d2..bf80820543102483ae5c41a301ad1ab0d322890d 100644
--- a/substrate/client/rpc/src/state/state_full.rs
+++ b/substrate/client/rpc/src/state/state_full.rs
@@ -33,7 +33,7 @@ use sp_core::{
 };
 use sp_version::RuntimeVersion;
 use sp_runtime::{
-	generic::BlockId, traits::{Block as BlockT, NumberFor, SaturatedConversion},
+	generic::BlockId, traits::{Block as BlockT, NumberFor, SaturatedConversion, CheckedSub},
 };
 
 use sp_api::{Metadata, ProvideRuntimeApi, CallApiAt};
@@ -94,8 +94,8 @@ impl<BE, Block: BlockT, Client> FullState<BE, Block, Client>
 		let from_meta = self.client.header_metadata(from).map_err(invalid_block_err)?;
 		let to_meta = self.client.header_metadata(to).map_err(invalid_block_err)?;
 
-		if from_meta.number >= to_meta.number {
-			return Err(invalid_block_range(&from_meta, &to_meta, "from number >= to number".to_owned()))
+		if from_meta.number > to_meta.number {
+			return Err(invalid_block_range(&from_meta, &to_meta, "from number > to number".to_owned()))
 		}
 
 		// check if we can get from `to` to `from` by going through parent_hashes.
@@ -122,7 +122,10 @@ impl<BE, Block: BlockT, Client> FullState<BE, Block, Client>
 			.max_key_changes_range(from_number, BlockId::Hash(to_meta.hash))
 			.map_err(client_err)?;
 		let filtered_range_begin = changes_trie_range
-			.map(|(begin, _)| (begin - from_number).saturated_into::<usize>());
+			.and_then(|(begin, _)| {
+				// avoids a corner case where begin < from_number (happens when querying genesis)
+				begin.checked_sub(&from_number).map(|x| x.saturated_into::<usize>())
+			});
 		let (unfiltered_range, filtered_range) = split_range(hashes.len(), filtered_range_begin);
 
 		Ok(QueryStorageRange {
@@ -398,6 +401,15 @@ impl<BE, Block, Client> StateBackend<Block, Client> for FullState<BE, Block, Cli
 		Box::new(result(call_fn()))
 	}
 
+	fn query_storage_at(
+		&self,
+		keys: Vec<StorageKey>,
+		at: Option<Block::Hash>
+	) -> FutureResult<Vec<StorageChangeSet<Block::Hash>>> {
+		let at = at.unwrap_or_else(|| self.client.info().best_hash);
+		self.query_storage(at, Some(at), keys)
+	}
+
 	fn subscribe_runtime_version(
 		&self,
 		_meta: crate::metadata::Metadata,
diff --git a/substrate/client/rpc/src/state/state_light.rs b/substrate/client/rpc/src/state/state_light.rs
index 59c0f2183cf898f0677ec9e39b843378244a7fda..092419ad0129e6f4109a19d3f916dc0eb24f5738 100644
--- a/substrate/client/rpc/src/state/state_light.rs
+++ b/substrate/client/rpc/src/state/state_light.rs
@@ -331,6 +331,14 @@ impl<Block, F, Client> StateBackend<Block, Client> for LightState<Block, F, Clie
 		Box::new(result(Err(client_err(ClientError::NotAvailableOnLightClient))))
 	}
 
+	fn query_storage_at(
+		&self,
+		_keys: Vec<StorageKey>,
+		_at: Option<Block::Hash>
+	) -> FutureResult<Vec<StorageChangeSet<Block::Hash>>> {
+		Box::new(result(Err(client_err(ClientError::NotAvailableOnLightClient))))
+	}
+
 	fn subscribe_storage(
 		&self,
 		_meta: crate::metadata::Metadata,
diff --git a/substrate/client/rpc/src/state/tests.rs b/substrate/client/rpc/src/state/tests.rs
index c7b5f88215ec375cc00385af4d954157bb61a8f9..4a9b701959c8c292a3b4dba25d89762fedfd8f62 100644
--- a/substrate/client/rpc/src/state/tests.rs
+++ b/substrate/client/rpc/src/state/tests.rs
@@ -30,6 +30,7 @@ use substrate_test_runtime_client::{
 	sp_consensus::BlockOrigin,
 	runtime,
 };
+use sp_runtime::generic::BlockId;
 
 const CHILD_INFO: ChildInfo<'static> = ChildInfo::new_default(b"unique_id");
 
@@ -212,7 +213,7 @@ fn should_send_initial_storage_changes_and_notifications() {
 
 #[test]
 fn should_query_storage() {
-	fn run_tests(mut client: Arc<TestClient>) {
+	fn run_tests(mut client: Arc<TestClient>, has_changes_trie_config: bool) {
 		let core = tokio::runtime::Runtime::new().unwrap();
 		let api = new_full(client.clone(), Subscriptions::new(Arc::new(core.executor())));
 
@@ -237,6 +238,13 @@ fn should_query_storage() {
 		let block2_hash = add_block(1);
 		let genesis_hash = client.genesis_hash();
 
+		if has_changes_trie_config {
+			assert_eq!(
+				client.max_key_changes_range(1, BlockId::Hash(block1_hash)).unwrap(),
+				Some((0, BlockId::Hash(block1_hash))),
+			);
+		}
+
 		let mut expected = vec![
 			StorageChangeSet {
 				block: genesis_hash,
@@ -306,7 +314,7 @@ fn should_query_storage() {
 			Err(Error::InvalidBlockRange {
 				from: format!("1 ({:?})", block1_hash),
 				to: format!("0 ({:?})", genesis_hash),
-				details: "from number >= to number".to_owned(),
+				details: "from number > to number".to_owned(),
 			}).map_err(|e| e.to_string())
 		);
 
@@ -376,12 +384,39 @@ fn should_query_storage() {
 				details: format!("UnknownBlock: header not found in db: {}", random_hash1),
 			}).map_err(|e| e.to_string()),
 		);
+
+		// single block range
+		let result = api.query_storage_at(
+			keys.clone(),
+			Some(block1_hash),
+		);
+
+		assert_eq!(
+			result.wait().unwrap(),
+			vec![
+				StorageChangeSet {
+					block: block1_hash,
+					changes: vec![
+						(StorageKey(vec![1_u8]), None),
+						(StorageKey(vec![2_u8]), Some(StorageData(vec![2_u8]))),
+						(StorageKey(vec![3_u8]), Some(StorageData(vec![3_u8]))),
+						(StorageKey(vec![4_u8]), None),
+						(StorageKey(vec![5_u8]), Some(StorageData(vec![0_u8]))),
+					]
+				}
+			]
+		);
 	}
 
-	run_tests(Arc::new(substrate_test_runtime_client::new()));
-	run_tests(Arc::new(TestClientBuilder::new()
-		.changes_trie_config(Some(ChangesTrieConfiguration::new(4, 2)))
-		.build()));
+	run_tests(Arc::new(substrate_test_runtime_client::new()), false);
+	run_tests(
+		Arc::new(
+			TestClientBuilder::new()
+				.changes_trie_config(Some(ChangesTrieConfiguration::new(4, 2)))
+				.build(),
+		),
+		true,
+	);
 }
 
 #[test]