Make CallApiAt::call_api_at work at light client (#1213)

* light client contextual method call

* fixed off-by-one in test

* newlines + commas
This commit is contained in:
Svyatoslav Nikolsky
2018-12-10 16:02:00 +03:00
committed by GitHub
parent acf1b77bcd
commit 742cb33d90
7 changed files with 341 additions and 132 deletions
+154 -38
View File
@@ -17,15 +17,17 @@
//! Light client call exector. Executes methods on remote full nodes, fetching
//! execution proof and checking it locally.
use std::collections::HashSet;
use std::marker::PhantomData;
use std::sync::Arc;
use futures::{IntoFuture, Future};
use primitives::convert_hash;
use codec::Encode;
use primitives::{H256, Blake2Hasher, convert_hash};
use runtime_primitives::generic::BlockId;
use runtime_primitives::traits::{Block as BlockT, Header as HeaderT};
use state_machine::{Backend as StateBackend, CodeExecutor, OverlayedChanges,
execution_proof_check, ExecutionManager};
use runtime_primitives::traits::{As, Block as BlockT, Header as HeaderT};
use state_machine::{self, Backend as StateBackend, CodeExecutor, OverlayedChanges,
create_proof_check_backend, execution_proof_check_on_trie_backend, ExecutionManager};
use hash_db::Hasher;
use blockchain::Backend as ChainBackend;
@@ -73,11 +75,7 @@ where
type Error = ClientError;
fn call(&self, id: &BlockId<Block>, method: &str, call_data: &[u8]) -> ClientResult<CallResult> {
let block_hash = match *id {
BlockId::Hash(hash) => hash,
BlockId::Number(number) => self.blockchain.hash(number)?
.ok_or_else(|| ClientErrorKind::UnknownBlock(format!("{}", number)))?,
};
let block_hash = self.blockchain.expect_block_hash_from_id(id)?;
let block_header = self.blockchain.expect_header(id.clone())?;
self.fetcher.remote_call(RemoteCallRequest {
@@ -89,6 +87,27 @@ where
}).into_future().wait()
}
fn contextual_call<
PB: Fn() -> ClientResult<Block::Header>,
EM: Fn(Result<Vec<u8>, Self::Error>, Result<Vec<u8>, Self::Error>) -> Result<Vec<u8>, Self::Error>,
>(
&self,
at: &BlockId<Block>,
method: &str,
call_data: &[u8],
changes: &mut OverlayedChanges,
initialised_block: &mut Option<BlockId<Block>>,
_prepare_environment_block: PB,
_manager: ExecutionManager<EM>,
) -> ClientResult<Vec<u8>> where ExecutionManager<EM>: Clone {
// it is only possible to execute contextual call if changes are empty
if !changes.is_empty() || initialised_block.is_some() {
return Err(ClientErrorKind::NotAvailableOnLightClient.into());
}
self.call(at, method, call_data).map(|cr| cr.return_data)
}
fn runtime_version(&self, id: &BlockId<Block>) -> ClientResult<RuntimeVersion> {
let call_result = self.call(id, "version", &[])?;
RuntimeVersion::decode(&mut call_result.return_data.as_slice())
@@ -108,9 +127,9 @@ where
Err(ClientErrorKind::NotAvailableOnLightClient.into())
}
fn prove_at_state<S: StateBackend<H>>(
fn prove_at_trie_state<S: state_machine::TrieBackendStorage<H>>(
&self,
_state: S,
_state: &state_machine::TrieBackend<S, H>,
_changes: &mut OverlayedChanges,
_method: &str,
_call_data: &[u8]
@@ -123,7 +142,49 @@ where
}
}
/// Check remote execution proof using given backend.
/// Prove contextual execution using given block header in environment.
///
/// Method is executed using passed header as environment' current block.
/// Proof includes both environment preparation proof and method execution proof.
pub fn prove_execution<Block, S, E>(
state: S,
header: Block::Header,
executor: &E,
method: &str,
call_data: &[u8],
) -> ClientResult<(Vec<u8>, Vec<Vec<u8>>)>
where
Block: BlockT<Hash=H256>,
S: StateBackend<Blake2Hasher>,
E: CallExecutor<Block, Blake2Hasher>,
{
let trie_state = state.try_into_trie_backend()
.ok_or_else(|| Box::new(state_machine::ExecutionError::UnableToGenerateProof) as Box<state_machine::Error>)?;
// prepare execution environment + record preparation proof
let mut changes = Default::default();
let (_, init_proof) = executor.prove_at_trie_state(
&trie_state,
&mut changes,
"Core_initialise_block",
&header.encode(),
)?;
// execute method + record execution proof
let (result, exec_proof) = executor.prove_at_trie_state(&trie_state, &mut changes, method, call_data)?;
let total_proof = init_proof.into_iter()
.chain(exec_proof.into_iter())
.collect::<HashSet<_>>()
.into_iter()
.collect();
Ok((result, total_proof))
}
/// Check remote contextual execution proof using given backend.
///
/// Method is executed using passed header as environment' current block.
/// Proof shoul include both environment preparation proof and method execution proof.
pub fn check_execution_proof<Header, E, H>(
executor: &E,
request: &RemoteCallRequest<Header>,
@@ -134,54 +195,109 @@ pub fn check_execution_proof<Header, E, H>(
E: CodeExecutor<H>,
H: Hasher,
H::Out: Ord + HeapSizeOf,
{
let local_state_root = request.header.state_root();
let root: H::Out = convert_hash(&local_state_root);
// prepare execution environment + check preparation proof
let mut changes = OverlayedChanges::default();
let local_result = execution_proof_check::<H, _>(
root,
remote_proof,
let trie_backend = create_proof_check_backend(root, remote_proof)?;
let next_block = <Header as HeaderT>::new(
*request.header.number() + As::sa(1),
Default::default(),
Default::default(),
request.header.hash(),
Default::default(),
);
execution_proof_check_on_trie_backend::<H, _>(
&trie_backend,
&mut changes,
executor,
"Core_initialise_block",
&next_block.encode(),
)?;
// execute method
let local_result = execution_proof_check_on_trie_backend::<H, _>(
&trie_backend,
&mut changes,
executor,
&request.method,
&request.call_data)?;
&request.call_data,
)?;
Ok(CallResult { return_data: local_result, changes })
}
#[cfg(test)]
mod tests {
use test_client;
use consensus::BlockOrigin;
use test_client::{self, runtime::{Block, Header}, runtime::RuntimeApi, TestClient};
use executor::NativeExecutionDispatch;
use super::*;
#[test]
fn execution_proof_is_generated_and_checked() {
type TestClient = test_client::client::Client<
test_client::Backend,
test_client::Executor,
Block,
RuntimeApi
>;
fn execute(remote_client: &TestClient, at: u64, method: &'static str) -> (Vec<u8>, Vec<u8>) {
let remote_block_id = BlockId::Number(at);
let remote_root = remote_client.state_at(&remote_block_id)
.unwrap().storage_root(::std::iter::empty()).0;
// 'fetch' execution proof from remote node
let (remote_result, remote_execution_proof) = remote_client.execution_proof(
&remote_block_id,
method,
&[]
).unwrap();
// check remote execution proof locally
let local_executor = test_client::LocalExecutor::new();
let local_result = check_execution_proof(&local_executor, &RemoteCallRequest {
block: test_client::runtime::Hash::default(),
header: test_client::runtime::Header {
state_root: remote_root.into(),
parent_hash: Default::default(),
number: at,
extrinsics_root: Default::default(),
digest: Default::default(),
},
method: method.into(),
call_data: vec![],
retry_count: None,
}, remote_execution_proof).unwrap();
(remote_result, local_result.return_data)
}
// prepare remote client
let remote_client = test_client::new();
let remote_block_id = BlockId::Number(0);
let remote_block_storage_root = remote_client.state_at(&remote_block_id)
.unwrap().storage_root(::std::iter::empty()).0;
for _ in 1..3 {
remote_client.import_justified(
BlockOrigin::Own,
remote_client.new_block().unwrap().bake().unwrap(),
Default::default(),
).unwrap();
}
// 'fetch' execution proof from remote node
let remote_execution_proof = remote_client.execution_proof(&remote_block_id, "Core_authorities", &[]).unwrap().1;
// check method that doesn't requires environment
let (remote, local) = execute(&remote_client, 0, "Core_authorities");
assert_eq!(remote, local);
// check remote execution proof locally
let local_executor = test_client::LocalExecutor::new();
check_execution_proof(&local_executor, &RemoteCallRequest {
block: test_client::runtime::Hash::default(),
header: test_client::runtime::Header {
state_root: remote_block_storage_root.into(),
parent_hash: Default::default(),
number: 0,
extrinsics_root: Default::default(),
digest: Default::default(),
},
method: "Core_authorities".into(),
call_data: vec![],
retry_count: None,
}, remote_execution_proof).unwrap();
// check method that requires environment
let (_, block) = execute(&remote_client, 0, "BlockBuilder_finalise_block");
let local_block: Header = Decode::decode(&mut &block[..]).unwrap();
assert_eq!(local_block.number, 1);
// check method that requires environment
let (_, block) = execute(&remote_client, 2, "BlockBuilder_finalise_block");
let local_block: Header = Decode::decode(&mut &block[..]).unwrap();
assert_eq!(local_block.number, 3);
}
}