From d8435e19dcf4dcc43ff8fe962bd9a685b14f2e92 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Fri, 22 May 2020 14:52:00 -0400 Subject: [PATCH] RPC Module for Relays (#80) * Test defining an RPC API * Add wrapper clients for the RPC API * Implement most Ethereum RPCs Does not include RPCs that require the bridge contract. * Implement a few of the Substrate RPCs Still missing proper error handling, as well as decoding responses from the Substrate node. * Make error handling more ergonomic * Implement Substrate RPCs that use `state_call` * Implement rest of Substrate RPCs * Implement `eth_call` RPC This can be used to implement higher level requests like fetching Substrate headers from an Ethereum node. * Build some of the higher level Ethereum RPCs Uses the new Ethereum RPC interface to do so * Build some of the higher level Substrate RPCs * Remove old Ethereum RPC methods * Remove old Substrate RPC methods * Add some documentation to Substrate RPCs * Fix typo in enum construction * Revert commits `0f0435d` to `ca75502` This range of commits was mainly trying to integrate the new RPC interface into the existing codebase, however this turned out to be a little out of scope for the current PR. Instead this work will be incorporated into a PR which aims to close #72. * Add documentation to RPCs * Rename functions in RPC API to conform to snake_case * Check that header contains a number and hash * Put doc comments on trait instead of impl methods * Remove expect() calls * Replace runtime API enums with consts * Accept Bytes when submitting extrinsic Let's us avoid using a runtime specific Extrinsic. * Add strictly typed arguments to RPC API Missing two methods right now, which require a `serde::Deserialize` implemenation before they can be changed. * Add `chain_getBlock` Substrate RPC * Use typed arguments for `eth_estimateGas` and `eth_call` * Silence dead code warnings * Add check for logs bloom * Remove unused variables * Add documentation to RPC error enums --- bridges/relays/ethereum/Cargo.toml | 1 + bridges/relays/ethereum/src/ethereum_types.rs | 3 + bridges/relays/ethereum/src/main.rs | 2 + bridges/relays/ethereum/src/rpc.rs | 301 ++++++++++++++++++ bridges/relays/ethereum/src/rpc_errors.rs | 98 ++++++ 5 files changed, 405 insertions(+) create mode 100644 bridges/relays/ethereum/src/rpc.rs create mode 100644 bridges/relays/ethereum/src/rpc_errors.rs diff --git a/bridges/relays/ethereum/Cargo.toml b/bridges/relays/ethereum/Cargo.toml index 4a05cba9420eb..ff603639e43cf 100644 --- a/bridges/relays/ethereum/Cargo.toml +++ b/bridges/relays/ethereum/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" ansi_term = "0.12" async-std = "1.0.1" async-stream = "0.2.0" +async-trait = "0.1.30" clap = { version = "2.33.1", features = ["yaml"] } codec = { package = "parity-scale-codec", version = "1.0.0" } env_logger = "0.7.0" diff --git a/bridges/relays/ethereum/src/ethereum_types.rs b/bridges/relays/ethereum/src/ethereum_types.rs index fcd87952e40d5..46ba869ac2a59 100644 --- a/bridges/relays/ethereum/src/ethereum_types.rs +++ b/bridges/relays/ethereum/src/ethereum_types.rs @@ -43,6 +43,9 @@ pub type EthereumHeaderId = HeaderId; /// Queued ethereum header ID. pub type QueuedEthereumHeader = QueuedHeader; +/// A raw Ethereum transaction that's been signed. +pub type SignedRawTx = Vec; + /// Ethereum synchronization pipeline. #[derive(Clone, Copy, Debug)] #[cfg_attr(test, derive(PartialEq))] diff --git a/bridges/relays/ethereum/src/main.rs b/bridges/relays/ethereum/src/main.rs index ae46468f8fa5e..c17582e23cb0c 100644 --- a/bridges/relays/ethereum/src/main.rs +++ b/bridges/relays/ethereum/src/main.rs @@ -21,6 +21,8 @@ mod ethereum_deploy_contract; mod ethereum_sync_loop; mod ethereum_types; mod headers; +mod rpc; +mod rpc_errors; mod substrate_client; mod substrate_sync_loop; mod substrate_types; diff --git a/bridges/relays/ethereum/src/rpc.rs b/bridges/relays/ethereum/src/rpc.rs new file mode 100644 index 0000000000000..70c775a268fbe --- /dev/null +++ b/bridges/relays/ethereum/src/rpc.rs @@ -0,0 +1,301 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +//! RPC Module + +#![allow(dead_code)] +#![allow(unused_variables)] +#[warn(missing_docs)] +use std::result; + +use crate::ethereum_client::EthereumConnectionParams; +use crate::ethereum_types::{ + Address as EthAddress, Bytes, CallRequest, EthereumHeaderId, Header as EthereumHeader, Receipt, SignedRawTx, + TransactionHash as EthereumTxHash, H256, U256, U64, +}; +use crate::rpc_errors::{EthereumNodeError, RpcError}; +use crate::substrate_client::SubstrateConnectionParams; +use crate::substrate_types::{ + Hash as SubstrateHash, Header as SubstrateHeader, Number as SubBlockNumber, SignedBlock as SubstrateBlock, +}; +use crate::sync_types::HeaderId; + +use async_trait::async_trait; +use codec::{Decode, Encode}; +use jsonrpsee::raw::client::RawClient; +use jsonrpsee::transport::http::HttpTransportClient; +use sp_bridge_eth_poa::Header as SubstrateEthereumHeader; + +const ETH_API_BEST_BLOCK: &str = "EthereumHeadersApi_best_block"; +const ETH_API_IMPORT_REQUIRES_RECEIPTS: &str = "EthereumHeadersApi_is_import_requires_receipts"; +const ETH_API_IS_KNOWN_BLOCK: &str = "EthereumHeadersApi_is_known_block"; +const SUB_API_GRANDPA_AUTHORITIES: &str = "GrandpaApi_grandpa_authorities"; + +type Result = result::Result; +type GrandpaAuthorityList = Vec; + +jsonrpsee::rpc_api! { + Ethereum { + #[rpc(method = "eth_estimateGas")] + fn estimate_gas(call_request: CallRequest) -> U256; + #[rpc(method = "eth_blockNumber")] + fn block_number() -> U64; + #[rpc(method = "eth_getBlockByNumber")] + fn get_block_by_number(block_number: u64) -> EthereumHeader; + #[rpc(method = "eth_getBlockByHash")] + fn get_block_by_hash(hash: H256) -> EthereumHeader; + #[rpc(method = "eth_getTransactionReceipt")] + fn get_transaction_receipt(transaction_hash: H256) -> Receipt; + #[rpc(method = "eth_getTransactionCount")] + fn get_transaction_count(address: EthAddress) -> U256; + #[rpc(method = "eth_submitTransaction")] + fn submit_transaction(transaction: Bytes) -> EthereumTxHash; + #[rpc(method = "eth_call")] + fn call(transaction_call: CallRequest) -> Bytes; + } + + Substrate { + #[rpc(method = "chain_getHeader")] + fn chain_get_header(block_hash: Option) -> SubstrateHeader; + #[rpc(method = "chain_getBlock")] + fn chain_get_block(block_hash: Option) -> SubstrateBlock; + #[rpc(method = "chain_getBlockHash")] + fn chain_get_block_hash(block_number: Option) -> SubstrateHash; + #[rpc(method = "system_accountNextIndex")] + fn system_account_next_index(account_id: node_primitives::AccountId) -> node_primitives::Index; + #[rpc(method = "author_submitExtrinsic")] + fn author_submit_extrinsic(extrinsic: Bytes) -> SubstrateHash; + #[rpc(method = "state_call")] + fn state_call(method: String, data: Bytes, at_block: Option) -> Bytes; + } +} + +/// The API for the supported Ethereum RPC methods. +#[async_trait] +pub trait EthereumRpc { + /// Estimate gas usage for the given call. + async fn estimate_gas(&mut self, call_request: CallRequest) -> Result; + /// Retrieve number of the best known block from the Ethereum node. + async fn best_block_number(&mut self) -> Result; + /// Retrieve block header by its number from Ethereum node. + async fn header_by_number(&mut self, block_number: u64) -> Result; + /// Retrieve block header by its hash from Ethereum node. + async fn header_by_hash(&mut self, hash: H256) -> Result; + /// Retrieve transaction receipt by transaction hash. + async fn transaction_receipt(&mut self, transaction_hash: H256) -> Result; + /// Get the nonce of the given account. + async fn account_nonce(&mut self, address: EthAddress) -> Result; + /// Submit an Ethereum transaction. + /// + /// The transaction must already be signed before sending it through this method. + async fn submit_transaction(&mut self, signed_raw_tx: SignedRawTx) -> Result; + /// Submit a call to an Ethereum smart contract. + async fn eth_call(&mut self, call_transaction: CallRequest) -> Result; +} + +/// The client used to interact with an Ethereum node through RPC. +pub struct EthereumRpcClient { + client: RawClient, +} + +impl EthereumRpcClient { + /// Create a new Ethereum RPC Client. + pub fn new(params: EthereumConnectionParams) -> Self { + let uri = format!("http://{}:{}", params.host, params.port); + let transport = HttpTransportClient::new(&uri); + let client = RawClient::new(transport); + + Self { client } + } +} + +#[async_trait] +impl EthereumRpc for EthereumRpcClient { + async fn estimate_gas(&mut self, call_request: CallRequest) -> Result { + Ok(Ethereum::estimate_gas(&mut self.client, call_request).await?) + } + + async fn best_block_number(&mut self) -> Result { + Ok(Ethereum::block_number(&mut self.client).await?.as_u64()) + } + + async fn header_by_number(&mut self, block_number: u64) -> Result { + let header = Ethereum::get_block_by_number(&mut self.client, block_number).await?; + match header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some() { + true => Ok(header), + false => Err(RpcError::Ethereum(EthereumNodeError::IncompleteHeader)), + } + } + + async fn header_by_hash(&mut self, hash: H256) -> Result { + let header = Ethereum::get_block_by_hash(&mut self.client, hash).await?; + match header.number.is_some() && header.hash.is_some() && header.logs_bloom.is_some() { + true => Ok(header), + false => Err(RpcError::Ethereum(EthereumNodeError::IncompleteHeader)), + } + } + + async fn transaction_receipt(&mut self, transaction_hash: H256) -> Result { + let receipt = Ethereum::get_transaction_receipt(&mut self.client, transaction_hash).await?; + + match receipt.gas_used { + Some(_) => Ok(receipt), + None => Err(RpcError::Ethereum(EthereumNodeError::IncompleteReceipt)), + } + } + + async fn account_nonce(&mut self, address: EthAddress) -> Result { + Ok(Ethereum::get_transaction_count(&mut self.client, address).await?) + } + + async fn submit_transaction(&mut self, signed_raw_tx: SignedRawTx) -> Result { + let transaction = Bytes(signed_raw_tx); + Ok(Ethereum::submit_transaction(&mut self.client, transaction).await?) + } + + async fn eth_call(&mut self, call_transaction: CallRequest) -> Result { + Ok(Ethereum::call(&mut self.client, call_transaction).await?) + } +} + +/// The API for the supported Substrate RPC methods. +#[async_trait] +pub trait SubstrateRpc { + /// Returns the best Substrate header. + async fn best_header(&mut self) -> Result; + /// Get a Substrate block from its hash. + async fn get_block(&mut self, block_hash: Option) -> Result; + /// Get a Substrate header by its hash. + async fn header_by_hash(&mut self, hash: SubstrateHash) -> Result; + /// Get a Substrate block hash by its number. + async fn block_hash_by_number(&mut self, number: SubBlockNumber) -> Result; + /// Get a Substrate header by its number. + async fn header_by_number(&mut self, block_number: SubBlockNumber) -> Result; + /// Get the nonce of the given Substrate account. + /// + /// Note: It's the caller's responsibility to make sure `account` is a valid ss58 address. + async fn next_account_index(&mut self, account: node_primitives::AccountId) -> Result; + /// Returns best Ethereum block that Substrate runtime knows of. + async fn best_ethereum_block(&mut self) -> Result; + /// Returns whether or not transactions receipts are required for Ethereum header submission. + async fn ethereum_receipts_required(&mut self, header: SubstrateEthereumHeader) -> Result; + /// Returns whether or not the given Ethereum header is known to the Substrate runtime. + async fn ethereum_header_known(&mut self, header_id: EthereumHeaderId) -> Result; + /// Submit an extrinsic for inclusion in a block. + /// + /// Note: The given transaction does not need be SCALE encoded beforehand. + async fn submit_extrinsic(&mut self, transaction: Bytes) -> Result; + /// Get the GRANDPA authority set at given block. + async fn grandpa_authorities_set(&mut self, block: SubstrateHash) -> Result; +} + +/// The client used to interact with a Substrate node through RPC. +pub struct SubstrateRpcClient { + client: RawClient, +} + +impl SubstrateRpcClient { + /// Create a new Substrate RPC Client. + pub fn new(params: SubstrateConnectionParams) -> Self { + let uri = format!("http://{}:{}", params.host, params.port); + let transport = HttpTransportClient::new(&uri); + let client = RawClient::new(transport); + + Self { client } + } +} + +#[async_trait] +impl SubstrateRpc for SubstrateRpcClient { + async fn best_header(&mut self) -> Result { + Ok(Substrate::chain_get_header(&mut self.client, None).await?) + } + + async fn get_block(&mut self, block_hash: Option) -> Result { + Ok(Substrate::chain_get_block(&mut self.client, block_hash).await?) + } + + async fn header_by_hash(&mut self, block_hash: SubstrateHash) -> Result { + Ok(Substrate::chain_get_header(&mut self.client, block_hash).await?) + } + + async fn block_hash_by_number(&mut self, number: SubBlockNumber) -> Result { + Ok(Substrate::chain_get_block_hash(&mut self.client, number).await?) + } + + async fn header_by_number(&mut self, block_number: SubBlockNumber) -> Result { + let block_hash = Self::block_hash_by_number(self, block_number).await?; + Ok(Self::header_by_hash(self, block_hash).await?) + } + + async fn next_account_index(&mut self, account: node_primitives::AccountId) -> Result { + Ok(Substrate::system_account_next_index(&mut self.client, account).await?) + } + + async fn best_ethereum_block(&mut self) -> Result { + let call = ETH_API_BEST_BLOCK.to_string(); + let data = Bytes("0x".into()); + + let encoded_response = Substrate::state_call(&mut self.client, call, data, None).await?; + let decoded_response: (u64, sp_bridge_eth_poa::H256) = Decode::decode(&mut &encoded_response.0[..])?; + + let best_header_id = HeaderId(decoded_response.0, decoded_response.1); + Ok(best_header_id) + } + + async fn ethereum_receipts_required(&mut self, header: SubstrateEthereumHeader) -> Result { + let call = ETH_API_IMPORT_REQUIRES_RECEIPTS.to_string(); + let data = Bytes(header.encode()); + + let encoded_response = Substrate::state_call(&mut self.client, call, data, None).await?; + let receipts_required: bool = Decode::decode(&mut &encoded_response.0[..])?; + + // Gonna make it the responsibility of the caller to return (receipts_required, id) + Ok(receipts_required) + } + + // The Substrate module could prune old headers. So this function could return false even + // if header is synced. And we'll mark corresponding Ethereum header as Orphan. + // + // But when we read the best header from Substrate next time, we will know that + // there's a better header. This Orphan will either be marked as synced, or + // eventually pruned. + async fn ethereum_header_known(&mut self, header_id: EthereumHeaderId) -> Result { + let call = ETH_API_IS_KNOWN_BLOCK.to_string(); + let data = Bytes(header_id.1.encode()); + + let encoded_response = Substrate::state_call(&mut self.client, call, data, None).await?; + let is_known_block: bool = Decode::decode(&mut &encoded_response.0[..])?; + + // Gonna make it the responsibility of the caller to return (is_known_block, id) + Ok(is_known_block) + } + + async fn submit_extrinsic(&mut self, transaction: Bytes) -> Result { + let encoded_transaction = Bytes(transaction.0.encode()); + Ok(Substrate::author_submit_extrinsic(&mut self.client, encoded_transaction).await?) + } + + async fn grandpa_authorities_set(&mut self, block: SubstrateHash) -> Result { + let call = SUB_API_GRANDPA_AUTHORITIES.to_string(); + let data = Bytes(block.as_bytes().to_vec()); + + let encoded_response = Substrate::state_call(&mut self.client, call, data, None).await?; + let authority_list = encoded_response.0; + + Ok(authority_list) + } +} diff --git a/bridges/relays/ethereum/src/rpc_errors.rs b/bridges/relays/ethereum/src/rpc_errors.rs new file mode 100644 index 0000000000000..86758b559920b --- /dev/null +++ b/bridges/relays/ethereum/src/rpc_errors.rs @@ -0,0 +1,98 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity Bridges Common is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity Bridges Common. If not, see . + +#![allow(dead_code)] + +use jsonrpsee::raw::client::RawClientError; +use jsonrpsee::transport::http::RequestError; +use serde_json; + +type RpcHttpError = RawClientError; + +/// Contains common errors that can occur when +/// interacting with a Substrate or Ethereum node +/// through RPC. +#[derive(Debug)] +pub enum RpcError { + /// The arguments to the RPC method failed to serialize. + Serialization(serde_json::Error), + /// An error occured when interacting with an Ethereum node. + Ethereum(EthereumNodeError), + /// An error occured when interacting with a Substrate node. + Substrate(SubstrateNodeError), + /// An error that can occur when making an HTTP request to + /// an JSON-RPC client. + Request(RpcHttpError), + /// The response from the client could not be SCALE decoded. + Decoding(codec::Error), +} + +impl From for RpcError { + fn from(err: serde_json::Error) -> Self { + Self::Serialization(err) + } +} + +impl From for RpcError { + fn from(err: EthereumNodeError) -> Self { + Self::Ethereum(err) + } +} + +impl From for RpcError { + fn from(err: SubstrateNodeError) -> Self { + Self::Substrate(err) + } +} + +impl From for RpcError { + fn from(err: RpcHttpError) -> Self { + Self::Request(err) + } +} + +impl From for RpcError { + fn from(err: codec::Error) -> Self { + Self::Decoding(err) + } +} + +/// Errors that can occur only when interacting with +/// an Ethereum node through RPC. +#[derive(Debug)] +pub enum EthereumNodeError { + /// Failed to parse response. + ResponseParseFailed(String), + /// We have received a header with missing fields. + IncompleteHeader, + /// We have received a receipt missing a `gas_used` field. + IncompleteReceipt, + /// An invalid Substrate block number was received from + /// an Ethereum node. + InvalidSubstrateBlockNumber, +} + +/// Errors that can occur only when interacting with +/// a Substrate node through RPC. +#[derive(Debug)] +pub enum SubstrateNodeError { + /// Request start failed. + StartRequestFailed(RequestError), + /// Error serializing request. + RequestSerialization(serde_json::Error), + /// Failed to parse response. + ResponseParseFailed, +}