fix: Complete snowbridge pezpallet rebrand and critical bug fixes
- snowbridge-pezpallet-* → pezsnowbridge-pezpallet-* (201 refs) - pallet/ directories → pezpallet/ (4 locations) - Fixed pezpallet.rs self-include recursion bug - Fixed sc-chain-spec hardcoded crate name in derive macro - Reverted .pezpallet_by_name() to .pallet_by_name() (subxt API) - Added BizinikiwiConfig type alias for zombienet tests - Deleted obsolete session state files Verified: pezsnowbridge-pezpallet-*, pezpallet-staking, pezpallet-staking-async, pezframe-benchmarking-cli all pass cargo check
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
// Copyright 2019-2023 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Basic runtime calls.
|
||||
|
||||
use codec::{Decode, Encode};
|
||||
use scale_info::TypeInfo;
|
||||
use pezsp_std::{boxed::Box, vec::Vec};
|
||||
|
||||
use xcm::{VersionedLocation, VersionedXcm};
|
||||
|
||||
/// A minimized version of `pezframe-system::Call` that can be used without a runtime.
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum SystemCall {
|
||||
/// `pezframe-system::Call::remark`
|
||||
#[codec(index = 1)]
|
||||
remark(Vec<u8>),
|
||||
}
|
||||
|
||||
/// A minimized version of `pezpallet-utility::Call` that can be used without a runtime.
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum UtilityCall<Call> {
|
||||
/// `pezpallet-utility::Call::batch_all`
|
||||
#[codec(index = 2)]
|
||||
batch_all(Vec<Call>),
|
||||
}
|
||||
|
||||
/// A minimized version of `pezpallet-sudo::Call` that can be used without a runtime.
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum SudoCall<Call> {
|
||||
/// `pezpallet-sudo::Call::sudo`
|
||||
#[codec(index = 0)]
|
||||
sudo(Box<Call>),
|
||||
}
|
||||
|
||||
/// A minimized version of `pezpallet-xcm::Call`, that can be used without a runtime.
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum XcmCall {
|
||||
/// `pezpallet-xcm::Call::send`
|
||||
#[codec(index = 0)]
|
||||
send(Box<VersionedLocation>, Box<VersionedXcm<()>>),
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::calls::UtilityCall;
|
||||
|
||||
use crate::SimpleRuntimeVersion;
|
||||
use bp_header_pez_chain::ChainWithGrandpa as ChainWithGrandpaBase;
|
||||
use bp_messages::ChainWithMessages as ChainWithMessagesBase;
|
||||
use pezbp_runtime::{
|
||||
Chain as ChainBase, EncodedOrDecodedCall, HashOf, Teyrchain as TeyrchainBase, TransactionEra,
|
||||
TransactionEraOf, UnderlyingChainProvider,
|
||||
};
|
||||
use codec::{Codec, Decode, Encode, MaxEncodedLen};
|
||||
use pezframe_support::Parameter;
|
||||
use jsonrpsee::core::{DeserializeOwned, Serialize};
|
||||
use num_traits::Zero;
|
||||
use pezsc_transaction_pool_api::TransactionStatus;
|
||||
use scale_info::TypeInfo;
|
||||
use pezsp_core::{storage::StorageKey, Pair};
|
||||
use pezsp_runtime::{
|
||||
generic::SignedBlock,
|
||||
traits::{AtLeast32BitUnsigned, Block as BlockT, Member},
|
||||
ConsensusEngineId, EncodedJustification,
|
||||
};
|
||||
use std::{fmt::Debug, time::Duration};
|
||||
|
||||
/// Signed block type of given chain.
|
||||
pub type SignedBlockOf<C> = <C as Chain>::SignedBlock;
|
||||
|
||||
/// Bizinikiwi-based chain from minimal relay-client point of view.
|
||||
pub trait Chain: ChainBase + Clone {
|
||||
/// Chain name.
|
||||
const NAME: &'static str;
|
||||
/// Name of the runtime API method that is returning best known finalized header number
|
||||
/// and hash (as tuple).
|
||||
///
|
||||
/// Keep in mind that this method is normally provided by the other chain, which is
|
||||
/// bridged with this chain.
|
||||
const BEST_FINALIZED_HEADER_ID_METHOD: &'static str;
|
||||
/// Name of the runtime API method that is returning interval between source chain
|
||||
/// headers that may be submitted for free to the target chain.
|
||||
///
|
||||
/// Keep in mind that this method is normally provided by the other chain, which is
|
||||
/// bridged with this chain.
|
||||
const FREE_HEADERS_INTERVAL_METHOD: &'static str;
|
||||
|
||||
/// Average block interval.
|
||||
///
|
||||
/// How often blocks are produced on that chain. It's suggested to set this value
|
||||
/// to match the block time of the chain.
|
||||
const AVERAGE_BLOCK_INTERVAL: Duration;
|
||||
|
||||
/// Block type.
|
||||
type SignedBlock: Member + Serialize + DeserializeOwned + BlockWithJustification<Self::Header>;
|
||||
/// The aggregated `Call` type.
|
||||
type Call: Clone + Codec + Debug + Send + Sync;
|
||||
}
|
||||
|
||||
/// Bridge-supported network definition.
|
||||
///
|
||||
/// Used to abstract away CLI commands.
|
||||
pub trait ChainWithRuntimeVersion: Chain {
|
||||
/// Current version of the chain runtime, known to relay.
|
||||
///
|
||||
/// can be `None` if relay is not going to submit transactions to that chain.
|
||||
const RUNTIME_VERSION: Option<SimpleRuntimeVersion>;
|
||||
}
|
||||
|
||||
/// Bizinikiwi-based relay chain that supports teyrchains.
|
||||
///
|
||||
/// We assume that the teyrchains are supported using `runtime_teyrchains::paras` pezpallet.
|
||||
pub trait RelayChain: Chain {
|
||||
/// Name of the `runtime_teyrchains::paras` pezpallet in the runtime of this chain.
|
||||
const PARAS_PALLET_NAME: &'static str;
|
||||
/// Name of the `pezpallet-bridge-teyrchains`, deployed at the **bridged** chain to sync
|
||||
/// teyrchains of **this** chain.
|
||||
const WITH_CHAIN_BRIDGE_TEYRCHAINS_PALLET_NAME: &'static str;
|
||||
}
|
||||
|
||||
/// Bizinikiwi-based chain that is using direct GRANDPA finality from minimal relay-client point of
|
||||
/// view.
|
||||
///
|
||||
/// Keep in mind that teyrchains are relying on relay chain GRANDPA, so they should not implement
|
||||
/// this trait.
|
||||
pub trait ChainWithGrandpa: Chain + ChainWithGrandpaBase {
|
||||
/// Name of the runtime API method that is returning the GRANDPA info associated with the
|
||||
/// headers accepted by the `submit_finality_proofs` extrinsic in the queried block.
|
||||
///
|
||||
/// Keep in mind that this method is normally provided by the other chain, which is
|
||||
/// bridged with this chain.
|
||||
const SYNCED_HEADERS_GRANDPA_INFO_METHOD: &'static str;
|
||||
|
||||
/// The type of the key owner proof used by the grandpa engine.
|
||||
type KeyOwnerProof: Decode + TypeInfo + Send;
|
||||
}
|
||||
|
||||
/// Bizinikiwi-based teyrchain from minimal relay-client point of view.
|
||||
pub trait Teyrchain: Chain + TeyrchainBase {}
|
||||
|
||||
impl<T> Teyrchain for T where T: UnderlyingChainProvider + Chain + TeyrchainBase {}
|
||||
|
||||
/// Bizinikiwi-based chain with messaging support from minimal relay-client point of view.
|
||||
pub trait ChainWithMessages: Chain + ChainWithMessagesBase {
|
||||
/// Name of the `To<ChainWithMessages>OutboundLaneApi::message_details` runtime API method.
|
||||
/// The method is provided by the runtime that is bridged with this `ChainWithMessages`.
|
||||
const TO_CHAIN_MESSAGE_DETAILS_METHOD: &'static str;
|
||||
|
||||
/// Name of the `From<ChainWithMessages>InboundLaneApi::message_details` runtime API method.
|
||||
/// The method is provided by the runtime that is bridged with this `ChainWithMessages`.
|
||||
const FROM_CHAIN_MESSAGE_DETAILS_METHOD: &'static str;
|
||||
}
|
||||
|
||||
/// Call type used by the chain.
|
||||
pub type CallOf<C> = <C as Chain>::Call;
|
||||
/// Transaction status of the chain.
|
||||
pub type TransactionStatusOf<C> = TransactionStatus<HashOf<C>, HashOf<C>>;
|
||||
|
||||
/// Bizinikiwi-based chain with `AccountData` generic argument of `pezframe_system::AccountInfo` set to
|
||||
/// the `pezpallet_balances::AccountData<Balance>`.
|
||||
pub trait ChainWithBalances: Chain {
|
||||
/// Return runtime storage key for getting `pezframe_system::AccountInfo` of given account.
|
||||
fn account_info_storage_key(account_id: &Self::AccountId) -> StorageKey;
|
||||
}
|
||||
|
||||
/// Bizinikiwi-based chain with bridge relayers pezpallet as a reward ledger.
|
||||
pub trait ChainWithRewards: Chain {
|
||||
/// Name of the bridge relayers pezpallet (used in `construct_runtime` macro call).
|
||||
const WITH_CHAIN_RELAYERS_PALLET_NAME: Option<&'static str>;
|
||||
/// Type of relayer reward balance.
|
||||
type RewardBalance: AtLeast32BitUnsigned + Copy + Member + Parameter + MaxEncodedLen;
|
||||
/// Reward discriminator type.
|
||||
type Reward: Parameter + MaxEncodedLen + Send + Sync + Copy + Clone;
|
||||
|
||||
/// Return runtime storage key for getting `reward_kind` of given account.
|
||||
fn account_reward_storage_key(
|
||||
account_id: &Self::AccountId,
|
||||
reward: impl Into<Self::Reward>,
|
||||
) -> StorageKey;
|
||||
}
|
||||
|
||||
/// SCALE-encoded extrinsic.
|
||||
pub type EncodedExtrinsic = Vec<u8>;
|
||||
|
||||
/// Block with justification.
|
||||
pub trait BlockWithJustification<Header> {
|
||||
/// Return block header.
|
||||
fn header(&self) -> Header;
|
||||
/// Return encoded block extrinsics.
|
||||
fn extrinsics(&self) -> Vec<EncodedExtrinsic>;
|
||||
/// Return block justification, if known.
|
||||
fn justification(&self, engine_id: ConsensusEngineId) -> Option<&EncodedJustification>;
|
||||
}
|
||||
|
||||
/// Transaction before it is signed.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct UnsignedTransaction<C: Chain> {
|
||||
/// Runtime call of this transaction.
|
||||
pub call: EncodedOrDecodedCall<C::Call>,
|
||||
/// Transaction nonce.
|
||||
pub nonce: C::Nonce,
|
||||
/// Tip included into transaction.
|
||||
pub tip: C::Balance,
|
||||
/// Transaction era used by the chain.
|
||||
pub era: TransactionEraOf<C>,
|
||||
}
|
||||
|
||||
impl<C: Chain> UnsignedTransaction<C> {
|
||||
/// Create new unsigned transaction with given call, nonce, era and zero tip.
|
||||
pub fn new(call: EncodedOrDecodedCall<C::Call>, nonce: C::Nonce) -> Self {
|
||||
Self { call, nonce, era: TransactionEra::Immortal, tip: Zero::zero() }
|
||||
}
|
||||
|
||||
/// Convert to the transaction of the other compatible chain.
|
||||
pub fn switch_chain<Other>(self) -> UnsignedTransaction<Other>
|
||||
where
|
||||
Other: Chain<
|
||||
Nonce = C::Nonce,
|
||||
Balance = C::Balance,
|
||||
BlockNumber = C::BlockNumber,
|
||||
Hash = C::Hash,
|
||||
>,
|
||||
{
|
||||
UnsignedTransaction {
|
||||
call: EncodedOrDecodedCall::Encoded(self.call.into_encoded()),
|
||||
nonce: self.nonce,
|
||||
tip: self.tip,
|
||||
era: self.era,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set transaction tip.
|
||||
#[must_use]
|
||||
pub fn tip(mut self, tip: C::Balance) -> Self {
|
||||
self.tip = tip;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set transaction era.
|
||||
#[must_use]
|
||||
pub fn era(mut self, era: TransactionEraOf<C>) -> Self {
|
||||
self.era = era;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Account key pair used by transactions signing scheme.
|
||||
pub type AccountKeyPairOf<S> = <S as ChainWithTransactions>::AccountKeyPair;
|
||||
|
||||
/// Bizinikiwi-based chain transactions signing scheme.
|
||||
pub trait ChainWithTransactions: Chain {
|
||||
/// Type of key pairs used to sign transactions.
|
||||
type AccountKeyPair: Pair + Clone + Send + Sync;
|
||||
/// Signed transaction.
|
||||
type SignedTransaction: Clone + Debug + Encode + Send + 'static;
|
||||
|
||||
/// Create transaction for given runtime call, signed by given account.
|
||||
fn sign_transaction(
|
||||
param: SignParam<Self>,
|
||||
unsigned: UnsignedTransaction<Self>,
|
||||
) -> Result<Self::SignedTransaction, crate::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
/// Sign transaction parameters
|
||||
pub struct SignParam<C: ChainWithTransactions> {
|
||||
/// Version of the runtime specification.
|
||||
pub spec_version: u32,
|
||||
/// Transaction version
|
||||
pub transaction_version: u32,
|
||||
/// Hash of the genesis block.
|
||||
pub genesis_hash: HashOf<C>,
|
||||
/// Signer account
|
||||
pub signer: AccountKeyPairOf<C>,
|
||||
}
|
||||
|
||||
impl<Block: BlockT> BlockWithJustification<Block::Header> for SignedBlock<Block> {
|
||||
fn header(&self) -> Block::Header {
|
||||
self.block.header().clone()
|
||||
}
|
||||
|
||||
fn extrinsics(&self) -> Vec<EncodedExtrinsic> {
|
||||
self.block.extrinsics().iter().map(Encode::encode).collect()
|
||||
}
|
||||
|
||||
fn justification(&self, engine_id: ConsensusEngineId) -> Option<&EncodedJustification> {
|
||||
self.justifications.as_ref().and_then(|j| j.get(engine_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait that provides functionality defined inside `pezpallet-utility`
|
||||
pub trait UtilityPallet<C: Chain> {
|
||||
/// Create batch call from given calls vector.
|
||||
fn build_batch_call(calls: Vec<C::Call>) -> C::Call;
|
||||
}
|
||||
|
||||
/// Structure that implements `UtilityPalletProvider` based on a full runtime.
|
||||
pub struct FullRuntimeUtilityPallet<R> {
|
||||
_phantom: std::marker::PhantomData<R>,
|
||||
}
|
||||
|
||||
impl<C, R> UtilityPallet<C> for FullRuntimeUtilityPallet<R>
|
||||
where
|
||||
C: Chain,
|
||||
R: pezpallet_utility::Config<RuntimeCall = C::Call>,
|
||||
<R as pezpallet_utility::Config>::RuntimeCall: From<pezpallet_utility::Call<R>>,
|
||||
{
|
||||
fn build_batch_call(calls: Vec<C::Call>) -> C::Call {
|
||||
pezpallet_utility::Call::batch_all { calls }.into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Structure that implements `UtilityPalletProvider` based on a call conversion.
|
||||
pub struct MockedRuntimeUtilityPallet<Call> {
|
||||
_phantom: std::marker::PhantomData<Call>,
|
||||
}
|
||||
|
||||
impl<C, Call> UtilityPallet<C> for MockedRuntimeUtilityPallet<Call>
|
||||
where
|
||||
C: Chain,
|
||||
C::Call: From<UtilityCall<C::Call>>,
|
||||
{
|
||||
fn build_batch_call(calls: Vec<C::Call>) -> C::Call {
|
||||
UtilityCall::batch_all(calls).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Bizinikiwi-based chain that uses `pezpallet-utility`.
|
||||
pub trait ChainWithUtilityPallet: Chain {
|
||||
/// The utility pezpallet provider.
|
||||
type UtilityPallet: UtilityPallet<Self>;
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Client implementation that is caching (whenever possible) results of its backend
|
||||
//! method calls.
|
||||
|
||||
use crate::{
|
||||
client::{Client, SubscriptionBroadcaster},
|
||||
error::{Error, Result},
|
||||
AccountIdOf, AccountKeyPairOf, BlockNumberOf, Chain, ChainWithGrandpa, ChainWithTransactions,
|
||||
HashOf, HeaderIdOf, HeaderOf, NonceOf, SignedBlockOf, SimpleRuntimeVersion, Subscription,
|
||||
TransactionTracker, UnsignedTransaction, ANCIENT_BLOCK_THRESHOLD,
|
||||
};
|
||||
use std::{cmp::Ordering, future::Future, task::Poll};
|
||||
|
||||
use async_std::{
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
task::JoinHandle,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use codec::Encode;
|
||||
use pezframe_support::weights::Weight;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use quick_cache::unsync::Cache;
|
||||
use pezsp_consensus_grandpa::{AuthorityId, OpaqueKeyOwnershipProof, SetId};
|
||||
use pezsp_core::{
|
||||
storage::{StorageData, StorageKey},
|
||||
Bytes, Pair,
|
||||
};
|
||||
use pezsp_runtime::{traits::Header as _, transaction_validity::TransactionValidity};
|
||||
use pezsp_trie::StorageProof;
|
||||
use pezsp_version::RuntimeVersion;
|
||||
|
||||
/// `quick_cache::unsync::Cache` wrapped in async-aware synchronization primitives.
|
||||
type SyncCache<K, V> = Arc<RwLock<Cache<K, V>>>;
|
||||
|
||||
/// Client implementation that is caching (whenever possible) results of its backend
|
||||
/// method calls. Apart from caching call results, it also supports some (at the
|
||||
/// moment: justifications) subscription sharing, meaning that the single server
|
||||
/// subscription may be shared by multiple subscribers at the client side.
|
||||
#[derive(Clone)]
|
||||
pub struct CachingClient<C: Chain, B: Client<C>> {
|
||||
backend: B,
|
||||
data: Arc<ClientData<C>>,
|
||||
}
|
||||
|
||||
/// Client data, shared by all `CachingClient` clones.
|
||||
struct ClientData<C: Chain> {
|
||||
grandpa_justifications: Arc<Mutex<Option<SubscriptionBroadcaster<Bytes>>>>,
|
||||
beefy_justifications: Arc<Mutex<Option<SubscriptionBroadcaster<Bytes>>>>,
|
||||
background_task_handle: Arc<Mutex<JoinHandle<Result<()>>>>,
|
||||
best_header: Arc<RwLock<Option<HeaderOf<C>>>>,
|
||||
best_finalized_header: Arc<RwLock<Option<HeaderOf<C>>>>,
|
||||
// `quick_cache::sync::Cache` has the `get_or_insert_async` method, which fits our needs,
|
||||
// but it uses synchronization primitives that are not aware of async execution. They
|
||||
// can block the executor threads and cause deadlocks => let's use primitives from
|
||||
// `async_std` crate around `quick_cache::unsync::Cache`
|
||||
header_hash_by_number_cache: SyncCache<BlockNumberOf<C>, HashOf<C>>,
|
||||
header_by_hash_cache: SyncCache<HashOf<C>, HeaderOf<C>>,
|
||||
block_by_hash_cache: SyncCache<HashOf<C>, SignedBlockOf<C>>,
|
||||
raw_storage_value_cache: SyncCache<(HashOf<C>, StorageKey), Option<StorageData>>,
|
||||
state_call_cache: SyncCache<(HashOf<C>, String, Bytes), Bytes>,
|
||||
}
|
||||
|
||||
impl<C: Chain, B: Client<C>> CachingClient<C, B> {
|
||||
/// Creates new `CachingClient` on top of given `backend`.
|
||||
pub async fn new(backend: B) -> Self {
|
||||
// most of relayer operations will never touch more than `ANCIENT_BLOCK_THRESHOLD`
|
||||
// headers, so we'll use this as a cache capacity for all chain-related caches
|
||||
let chain_state_capacity = ANCIENT_BLOCK_THRESHOLD as usize;
|
||||
let best_header = Arc::new(RwLock::new(None));
|
||||
let best_finalized_header = Arc::new(RwLock::new(None));
|
||||
let header_by_hash_cache = Arc::new(RwLock::new(Cache::new(chain_state_capacity)));
|
||||
let background_task_handle = Self::start_background_task(
|
||||
backend.clone(),
|
||||
best_header.clone(),
|
||||
best_finalized_header.clone(),
|
||||
header_by_hash_cache.clone(),
|
||||
)
|
||||
.await;
|
||||
CachingClient {
|
||||
backend,
|
||||
data: Arc::new(ClientData {
|
||||
grandpa_justifications: Arc::new(Mutex::new(None)),
|
||||
beefy_justifications: Arc::new(Mutex::new(None)),
|
||||
background_task_handle: Arc::new(Mutex::new(background_task_handle)),
|
||||
best_header,
|
||||
best_finalized_header,
|
||||
header_hash_by_number_cache: Arc::new(RwLock::new(Cache::new(
|
||||
chain_state_capacity,
|
||||
))),
|
||||
header_by_hash_cache,
|
||||
block_by_hash_cache: Arc::new(RwLock::new(Cache::new(chain_state_capacity))),
|
||||
raw_storage_value_cache: Arc::new(RwLock::new(Cache::new(1_024))),
|
||||
state_call_cache: Arc::new(RwLock::new(Cache::new(1_024))),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to get value from the cache, or compute and insert it using given future.
|
||||
async fn get_or_insert_async<K: Clone + std::fmt::Debug + Eq + std::hash::Hash, V: Clone>(
|
||||
&self,
|
||||
cache: &Arc<RwLock<Cache<K, V>>>,
|
||||
key: &K,
|
||||
with: impl std::future::Future<Output = Result<V>>,
|
||||
) -> Result<V> {
|
||||
// try to get cached value first using read lock
|
||||
{
|
||||
let cache = cache.read().await;
|
||||
if let Some(value) = cache.get(key) {
|
||||
return Ok(value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// let's compute the value without holding any locks - it may cause additional misses and
|
||||
// double insertions, but that's better than holding a lock for a while
|
||||
let value = with.await?;
|
||||
|
||||
// insert/update the value in the cache
|
||||
cache.write().await.insert(key.clone(), value.clone());
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
/// Subscribe to finality justifications, trying to reuse existing subscription.
|
||||
async fn subscribe_finality_justifications<'a>(
|
||||
&'a self,
|
||||
maybe_broadcaster: &Mutex<Option<SubscriptionBroadcaster<Bytes>>>,
|
||||
do_subscribe: impl Future<Output = Result<Subscription<Bytes>>> + 'a,
|
||||
) -> Result<Subscription<Bytes>> {
|
||||
let mut maybe_broadcaster = maybe_broadcaster.lock().await;
|
||||
let broadcaster = match maybe_broadcaster.as_ref() {
|
||||
Some(justifications) => justifications,
|
||||
None => {
|
||||
let broadcaster = match SubscriptionBroadcaster::new(do_subscribe.await?) {
|
||||
Ok(broadcaster) => broadcaster,
|
||||
Err(subscription) => return Ok(subscription),
|
||||
};
|
||||
maybe_broadcaster.get_or_insert(broadcaster)
|
||||
},
|
||||
};
|
||||
|
||||
broadcaster.subscribe().await
|
||||
}
|
||||
|
||||
/// Start background task that reads best (and best finalized) headers from subscriptions.
|
||||
async fn start_background_task(
|
||||
backend: B,
|
||||
best_header: Arc<RwLock<Option<HeaderOf<C>>>>,
|
||||
best_finalized_header: Arc<RwLock<Option<HeaderOf<C>>>>,
|
||||
header_by_hash_cache: SyncCache<HashOf<C>, HeaderOf<C>>,
|
||||
) -> JoinHandle<Result<()>> {
|
||||
async_std::task::spawn(async move {
|
||||
// initialize by reading headers directly from backend to avoid doing that in the
|
||||
// high-level code
|
||||
let mut last_finalized_header =
|
||||
backend.header_by_hash(backend.best_finalized_header_hash().await?).await?;
|
||||
*best_header.write().await = Some(backend.best_header().await?);
|
||||
*best_finalized_header.write().await = Some(last_finalized_header.clone());
|
||||
|
||||
// ...and then continue with subscriptions
|
||||
let mut best_headers = backend.subscribe_best_headers().await?;
|
||||
let mut finalized_headers = backend.subscribe_finalized_headers().await?;
|
||||
loop {
|
||||
futures::select! {
|
||||
new_best_header = best_headers.next().fuse() => {
|
||||
// we assume that the best header is always the actual best header, even if its
|
||||
// number is lower than the number of previous-best-header (chain may use its own
|
||||
// best header selection algorithms)
|
||||
let new_best_header = new_best_header
|
||||
.ok_or_else(|| Error::ChannelError(format!("Mandatory best headers subscription for {} has finished", C::NAME)))?;
|
||||
let new_best_header_hash = new_best_header.hash();
|
||||
header_by_hash_cache.write().await.insert(new_best_header_hash, new_best_header.clone());
|
||||
*best_header.write().await = Some(new_best_header);
|
||||
},
|
||||
new_finalized_header = finalized_headers.next().fuse() => {
|
||||
// in theory we'll always get finalized headers in order, but let's double check
|
||||
let new_finalized_header = new_finalized_header.
|
||||
ok_or_else(|| Error::ChannelError(format!("Finalized headers subscription for {} has finished", C::NAME)))?;
|
||||
let new_finalized_header_number = *new_finalized_header.number();
|
||||
let last_finalized_header_number = *last_finalized_header.number();
|
||||
match new_finalized_header_number.cmp(&last_finalized_header_number) {
|
||||
Ordering::Greater => {
|
||||
let new_finalized_header_hash = new_finalized_header.hash();
|
||||
header_by_hash_cache.write().await.insert(new_finalized_header_hash, new_finalized_header.clone());
|
||||
*best_finalized_header.write().await = Some(new_finalized_header.clone());
|
||||
last_finalized_header = new_finalized_header;
|
||||
},
|
||||
Ordering::Less => {
|
||||
return Err(Error::unordered_finalized_headers::<C>(
|
||||
new_finalized_header_number,
|
||||
last_finalized_header_number,
|
||||
));
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Ensure that the background task is active.
|
||||
async fn ensure_background_task_active(&self) -> Result<()> {
|
||||
let mut background_task_handle = self.data.background_task_handle.lock().await;
|
||||
if let Poll::Ready(result) = futures::poll!(&mut *background_task_handle) {
|
||||
return Err(Error::ChannelError(format!(
|
||||
"Background task of {} client has exited with result: {:?}",
|
||||
C::NAME,
|
||||
result
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Try to get header, read elsewhere by background task through subscription.
|
||||
async fn read_header_from_background<'a>(
|
||||
&'a self,
|
||||
header: &Arc<RwLock<Option<HeaderOf<C>>>>,
|
||||
read_header_from_backend: impl Future<Output = Result<HeaderOf<C>>> + 'a,
|
||||
) -> Result<HeaderOf<C>> {
|
||||
// ensure that the background task is active
|
||||
self.ensure_background_task_active().await?;
|
||||
|
||||
// now we know that the background task is active, so we could trust that the
|
||||
// `header` has the most recent updates from it
|
||||
match header.read().await.clone() {
|
||||
Some(header) => Ok(header),
|
||||
None => {
|
||||
// header has not yet been read from the subscription, which means that
|
||||
// we are just starting - let's read header directly from backend this time
|
||||
read_header_from_backend.await
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chain, B: Client<C>> std::fmt::Debug for CachingClient<C, B> {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.write_fmt(format_args!("CachingClient<{:?}>", self.backend))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: Chain, B: Client<C>> Client<C> for CachingClient<C, B> {
|
||||
async fn ensure_synced(&self) -> Result<()> {
|
||||
self.backend.ensure_synced().await
|
||||
}
|
||||
|
||||
async fn reconnect(&self) -> Result<()> {
|
||||
self.backend.reconnect().await?;
|
||||
// since we have new underlying client, we need to restart subscriptions too
|
||||
*self.data.grandpa_justifications.lock().await = None;
|
||||
*self.data.beefy_justifications.lock().await = None;
|
||||
// also restart background task too
|
||||
*self.data.best_header.write().await = None;
|
||||
*self.data.best_finalized_header.write().await = None;
|
||||
*self.data.background_task_handle.lock().await = Self::start_background_task(
|
||||
self.backend.clone(),
|
||||
self.data.best_header.clone(),
|
||||
self.data.best_finalized_header.clone(),
|
||||
self.data.header_by_hash_cache.clone(),
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn genesis_hash(&self) -> HashOf<C> {
|
||||
self.backend.genesis_hash()
|
||||
}
|
||||
|
||||
async fn header_hash_by_number(&self, number: BlockNumberOf<C>) -> Result<HashOf<C>> {
|
||||
self.get_or_insert_async(
|
||||
&self.data.header_hash_by_number_cache,
|
||||
&number,
|
||||
self.backend.header_hash_by_number(number),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn header_by_hash(&self, hash: HashOf<C>) -> Result<HeaderOf<C>> {
|
||||
self.get_or_insert_async(
|
||||
&self.data.header_by_hash_cache,
|
||||
&hash,
|
||||
self.backend.header_by_hash(hash),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn block_by_hash(&self, hash: HashOf<C>) -> Result<SignedBlockOf<C>> {
|
||||
self.get_or_insert_async(
|
||||
&self.data.block_by_hash_cache,
|
||||
&hash,
|
||||
self.backend.block_by_hash(hash),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn best_finalized_header_hash(&self) -> Result<HashOf<C>> {
|
||||
self.read_header_from_background(
|
||||
&self.data.best_finalized_header,
|
||||
self.backend.best_finalized_header(),
|
||||
)
|
||||
.await
|
||||
.map(|h| h.hash())
|
||||
}
|
||||
|
||||
async fn best_header(&self) -> Result<HeaderOf<C>> {
|
||||
self.read_header_from_background(&self.data.best_header, self.backend.best_header())
|
||||
.await
|
||||
}
|
||||
|
||||
async fn subscribe_best_headers(&self) -> Result<Subscription<HeaderOf<C>>> {
|
||||
// we may share the sunbscription here, but atm there's no callers of this method
|
||||
self.backend.subscribe_best_headers().await
|
||||
}
|
||||
|
||||
async fn subscribe_finalized_headers(&self) -> Result<Subscription<HeaderOf<C>>> {
|
||||
// we may share the sunbscription here, but atm there's no callers of this method
|
||||
self.backend.subscribe_finalized_headers().await
|
||||
}
|
||||
|
||||
async fn subscribe_grandpa_finality_justifications(&self) -> Result<Subscription<Bytes>>
|
||||
where
|
||||
C: ChainWithGrandpa,
|
||||
{
|
||||
self.subscribe_finality_justifications(
|
||||
&self.data.grandpa_justifications,
|
||||
self.backend.subscribe_grandpa_finality_justifications(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn generate_grandpa_key_ownership_proof(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
set_id: SetId,
|
||||
authority_id: AuthorityId,
|
||||
) -> Result<Option<OpaqueKeyOwnershipProof>> {
|
||||
self.backend
|
||||
.generate_grandpa_key_ownership_proof(at, set_id, authority_id)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn subscribe_beefy_finality_justifications(&self) -> Result<Subscription<Bytes>> {
|
||||
self.subscribe_finality_justifications(
|
||||
&self.data.beefy_justifications,
|
||||
self.backend.subscribe_beefy_finality_justifications(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn token_decimals(&self) -> Result<Option<u64>> {
|
||||
self.backend.token_decimals().await
|
||||
}
|
||||
|
||||
async fn runtime_version(&self) -> Result<RuntimeVersion> {
|
||||
self.backend.runtime_version().await
|
||||
}
|
||||
|
||||
async fn simple_runtime_version(&self) -> Result<SimpleRuntimeVersion> {
|
||||
self.backend.simple_runtime_version().await
|
||||
}
|
||||
|
||||
fn can_start_version_guard(&self) -> bool {
|
||||
self.backend.can_start_version_guard()
|
||||
}
|
||||
|
||||
async fn raw_storage_value(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
storage_key: StorageKey,
|
||||
) -> Result<Option<StorageData>> {
|
||||
self.get_or_insert_async(
|
||||
&self.data.raw_storage_value_cache,
|
||||
&(at, storage_key.clone()),
|
||||
self.backend.raw_storage_value(at, storage_key),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn pending_extrinsics(&self) -> Result<Vec<Bytes>> {
|
||||
self.backend.pending_extrinsics().await
|
||||
}
|
||||
|
||||
async fn submit_unsigned_extrinsic(&self, transaction: Bytes) -> Result<HashOf<C>> {
|
||||
self.backend.submit_unsigned_extrinsic(transaction).await
|
||||
}
|
||||
|
||||
async fn submit_signed_extrinsic(
|
||||
&self,
|
||||
signer: &AccountKeyPairOf<C>,
|
||||
prepare_extrinsic: impl FnOnce(HeaderIdOf<C>, NonceOf<C>) -> Result<UnsignedTransaction<C>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
) -> Result<HashOf<C>>
|
||||
where
|
||||
C: ChainWithTransactions,
|
||||
AccountIdOf<C>: From<<AccountKeyPairOf<C> as Pair>::Public>,
|
||||
{
|
||||
self.backend.submit_signed_extrinsic(signer, prepare_extrinsic).await
|
||||
}
|
||||
|
||||
async fn submit_and_watch_signed_extrinsic(
|
||||
&self,
|
||||
signer: &AccountKeyPairOf<C>,
|
||||
prepare_extrinsic: impl FnOnce(HeaderIdOf<C>, NonceOf<C>) -> Result<UnsignedTransaction<C>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
) -> Result<TransactionTracker<C, Self>>
|
||||
where
|
||||
C: ChainWithTransactions,
|
||||
AccountIdOf<C>: From<<AccountKeyPairOf<C> as Pair>::Public>,
|
||||
{
|
||||
self.backend
|
||||
.submit_and_watch_signed_extrinsic(signer, prepare_extrinsic)
|
||||
.await
|
||||
.map(|t| t.switch_environment(self.clone()))
|
||||
}
|
||||
|
||||
async fn validate_transaction<SignedTransaction: Encode + Send + 'static>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
transaction: SignedTransaction,
|
||||
) -> Result<TransactionValidity> {
|
||||
self.backend.validate_transaction(at, transaction).await
|
||||
}
|
||||
|
||||
async fn estimate_extrinsic_weight<SignedTransaction: Encode + Send + 'static>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
transaction: SignedTransaction,
|
||||
) -> Result<Weight> {
|
||||
self.backend.estimate_extrinsic_weight(at, transaction).await
|
||||
}
|
||||
|
||||
async fn raw_state_call<Args: Encode + Send>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
method: String,
|
||||
arguments: Args,
|
||||
) -> Result<Bytes> {
|
||||
let encoded_arguments = Bytes(arguments.encode());
|
||||
self.get_or_insert_async(
|
||||
&self.data.state_call_cache,
|
||||
&(at, method.clone(), encoded_arguments),
|
||||
self.backend.raw_state_call(at, method, arguments),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn prove_storage(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
keys: Vec<StorageKey>,
|
||||
) -> Result<(StorageProof, HashOf<C>)> {
|
||||
self.backend.prove_storage(at, keys).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Layered Bizinikiwi client implementation.
|
||||
|
||||
use crate::{Chain, ConnectionParams};
|
||||
|
||||
use caching::CachingClient;
|
||||
use num_traits::Saturating;
|
||||
use rpc::RpcClient;
|
||||
use pezsp_version::RuntimeVersion;
|
||||
|
||||
pub mod caching;
|
||||
pub mod rpc;
|
||||
|
||||
mod rpc_api;
|
||||
mod subscription;
|
||||
mod traits;
|
||||
|
||||
pub use subscription::{StreamDescription, Subscription, SubscriptionBroadcaster};
|
||||
pub use traits::Client;
|
||||
|
||||
/// Type of RPC client with caching support.
|
||||
pub type RpcWithCachingClient<C> = CachingClient<C, RpcClient<C>>;
|
||||
|
||||
/// Creates new RPC client with caching support.
|
||||
pub async fn rpc_with_caching<C: Chain>(params: ConnectionParams) -> RpcWithCachingClient<C> {
|
||||
let rpc = rpc::RpcClient::<C>::new(params).await;
|
||||
caching::CachingClient::new(rpc).await
|
||||
}
|
||||
|
||||
/// The difference between best block number and number of its ancestor, that is enough
|
||||
/// for us to consider that ancestor an "ancient" block with dropped state.
|
||||
///
|
||||
/// The relay does not assume that it is connected to the archive node, so it always tries
|
||||
/// to use the best available chain state. But sometimes it still may use state of some
|
||||
/// old block. If the state of that block is already dropped, relay will see errors when
|
||||
/// e.g. it tries to prove something.
|
||||
///
|
||||
/// By default Bizinikiwi-based nodes are storing state for last 256 blocks. We'll use
|
||||
/// half of this value.
|
||||
pub const ANCIENT_BLOCK_THRESHOLD: u32 = 128;
|
||||
|
||||
/// Returns `true` if we think that the state is already discarded for given block.
|
||||
pub fn is_ancient_block<N: From<u32> + PartialOrd + Saturating>(block: N, best: N) -> bool {
|
||||
best.saturating_sub(block) >= N::from(ANCIENT_BLOCK_THRESHOLD)
|
||||
}
|
||||
|
||||
/// Opaque GRANDPA authorities set.
|
||||
pub type OpaqueGrandpaAuthoritiesSet = Vec<u8>;
|
||||
|
||||
/// A simple runtime version. It only includes the `spec_version` and `transaction_version`.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct SimpleRuntimeVersion {
|
||||
/// Version of the runtime specification.
|
||||
pub spec_version: u32,
|
||||
/// All existing dispatches are fully compatible when this number doesn't change.
|
||||
pub transaction_version: u32,
|
||||
}
|
||||
|
||||
impl SimpleRuntimeVersion {
|
||||
/// Create a new instance of `SimpleRuntimeVersion` from a `RuntimeVersion`.
|
||||
pub const fn from_runtime_version(runtime_version: &RuntimeVersion) -> Self {
|
||||
Self {
|
||||
spec_version: runtime_version.spec_version,
|
||||
transaction_version: runtime_version.transaction_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Chain runtime version in client
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum ChainRuntimeVersion {
|
||||
/// Auto query from chain.
|
||||
Auto,
|
||||
/// Custom runtime version, defined by user.
|
||||
Custom(SimpleRuntimeVersion),
|
||||
}
|
||||
@@ -0,0 +1,743 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Client implementation that connects to the Bizinikiwi node over `ws`/`wss` connection
|
||||
//! and is using RPC methods to get required data and submit transactions.
|
||||
|
||||
use crate::{
|
||||
client::{
|
||||
rpc_api::{
|
||||
BizinikiwiAuthorClient, BizinikiwiBeefyClient, BizinikiwiChainClient,
|
||||
BizinikiwiFrameSystemClient, BizinikiwiGrandpaClient, BizinikiwiStateClient,
|
||||
BizinikiwiSystemClient,
|
||||
},
|
||||
subscription::{StreamDescription, Subscription},
|
||||
Client,
|
||||
},
|
||||
error::{Error, Result},
|
||||
guard::Environment,
|
||||
transaction_stall_timeout, AccountIdOf, AccountKeyPairOf, BalanceOf, BlockNumberOf, Chain,
|
||||
ChainRuntimeVersion, ChainWithGrandpa, ChainWithTransactions, ConnectionParams, HashOf,
|
||||
HeaderIdOf, HeaderOf, NonceOf, SignParam, SignedBlockOf, SimpleRuntimeVersion,
|
||||
TransactionTracker, UnsignedTransaction,
|
||||
};
|
||||
|
||||
use async_std::sync::{Arc, Mutex, RwLock};
|
||||
use async_trait::async_trait;
|
||||
use pezbp_runtime::HeaderIdProvider;
|
||||
use codec::Encode;
|
||||
use pezframe_support::weights::Weight;
|
||||
use futures::TryFutureExt;
|
||||
use jsonrpsee::{
|
||||
core::{client::Subscription as RpcSubscription, ClientError},
|
||||
ws_client::{WsClient, WsClientBuilder},
|
||||
};
|
||||
use num_traits::Zero;
|
||||
use pezpallet_transaction_payment::RuntimeDispatchInfo;
|
||||
use relay_utils::{relay_loop::RECONNECT_DELAY, STALL_TIMEOUT};
|
||||
use pezsp_core::{
|
||||
storage::{StorageData, StorageKey},
|
||||
Bytes, Hasher, Pair,
|
||||
};
|
||||
use pezsp_runtime::{
|
||||
traits::Header,
|
||||
transaction_validity::{TransactionSource, TransactionValidity},
|
||||
};
|
||||
use pezsp_trie::StorageProof;
|
||||
use pezsp_version::RuntimeVersion;
|
||||
use std::{cmp::Ordering, future::Future, marker::PhantomData};
|
||||
|
||||
const MAX_SUBSCRIPTION_CAPACITY: usize = 4096;
|
||||
|
||||
const SUB_API_TXPOOL_VALIDATE_TRANSACTION: &str = "TaggedTransactionQueue_validate_transaction";
|
||||
const SUB_API_TX_PAYMENT_QUERY_INFO: &str = "TransactionPaymentApi_query_info";
|
||||
const SUB_API_GRANDPA_GENERATE_KEY_OWNERSHIP_PROOF: &str =
|
||||
"GrandpaApi_generate_key_ownership_proof";
|
||||
|
||||
/// Client implementation that connects to the Bizinikiwi node over `ws`/`wss` connection
|
||||
/// and is using RPC methods to get required data and submit transactions.
|
||||
pub struct RpcClient<C: Chain> {
|
||||
// Lock order: `submit_signed_extrinsic_lock`, `data`
|
||||
/// Client connection params.
|
||||
params: Arc<ConnectionParams>,
|
||||
/// If several tasks are submitting their transactions simultaneously using
|
||||
/// `submit_signed_extrinsic` method, they may get the same transaction nonce. So one of
|
||||
/// transactions will be rejected from the pool. This lock is here to prevent situations like
|
||||
/// that.
|
||||
submit_signed_extrinsic_lock: Arc<Mutex<()>>,
|
||||
/// Genesis block hash.
|
||||
genesis_hash: HashOf<C>,
|
||||
/// Shared dynamic data.
|
||||
data: Arc<RwLock<ClientData>>,
|
||||
/// Generic arguments dump.
|
||||
_phantom: PhantomData<C>,
|
||||
}
|
||||
|
||||
/// Client data, shared by all `RpcClient` clones.
|
||||
struct ClientData {
|
||||
/// Tokio runtime handle.
|
||||
tokio: Arc<tokio::runtime::Runtime>,
|
||||
/// Bizinikiwi RPC client.
|
||||
client: Arc<WsClient>,
|
||||
}
|
||||
|
||||
/// Already encoded value.
|
||||
struct PreEncoded(Vec<u8>);
|
||||
|
||||
impl Encode for PreEncoded {
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chain> std::fmt::Debug for RpcClient<C> {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.write_fmt(format_args!("RpcClient<{}>", C::NAME))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chain> RpcClient<C> {
|
||||
/// Returns client that is able to call RPCs on Bizinikiwi node over websocket connection.
|
||||
///
|
||||
/// This function will keep connecting to given Bizinikiwi node until connection is established
|
||||
/// and is functional. If attempt fail, it will wait for `RECONNECT_DELAY` and retry again.
|
||||
pub async fn new(params: ConnectionParams) -> Self {
|
||||
let params = Arc::new(params);
|
||||
loop {
|
||||
match Self::try_connect(params.clone()).await {
|
||||
Ok(client) => return client,
|
||||
Err(error) => tracing::error!(
|
||||
target: "bridge",
|
||||
?error,
|
||||
node=%C::NAME,
|
||||
retry_as_secs=%RECONNECT_DELAY.as_secs(),
|
||||
"Failed to connect. Going to retry"
|
||||
),
|
||||
}
|
||||
|
||||
async_std::task::sleep(RECONNECT_DELAY).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to connect to Bizinikiwi node over websocket. Returns Bizinikiwi RPC client if connection
|
||||
/// has been established or error otherwise.
|
||||
async fn try_connect(params: Arc<ConnectionParams>) -> Result<Self> {
|
||||
let (tokio, client) = Self::build_client(¶ms).await?;
|
||||
|
||||
let genesis_hash_client = client.clone();
|
||||
let genesis_hash = tokio
|
||||
.spawn(async move {
|
||||
BizinikiwiChainClient::<C>::block_hash(&*genesis_hash_client, Some(Zero::zero()))
|
||||
.await
|
||||
})
|
||||
.await??;
|
||||
|
||||
let chain_runtime_version = params.chain_runtime_version;
|
||||
let mut client = Self {
|
||||
params,
|
||||
submit_signed_extrinsic_lock: Arc::new(Mutex::new(())),
|
||||
genesis_hash,
|
||||
data: Arc::new(RwLock::new(ClientData { tokio, client })),
|
||||
_phantom: PhantomData,
|
||||
};
|
||||
Self::ensure_correct_runtime_version(&mut client, chain_runtime_version).await?;
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
// Check runtime version to understand if we need are connected to expected version, or we
|
||||
// need to wait for upgrade, we need to abort immediately.
|
||||
async fn ensure_correct_runtime_version<E: Environment<C, Error = Error>>(
|
||||
env: &mut E,
|
||||
expected: ChainRuntimeVersion,
|
||||
) -> Result<()> {
|
||||
// we are only interested if version mode is bundled or passed using CLI
|
||||
let expected = match expected {
|
||||
ChainRuntimeVersion::Auto => return Ok(()),
|
||||
ChainRuntimeVersion::Custom(expected) => expected,
|
||||
};
|
||||
|
||||
// we need to wait if actual version is < than expected, we are OK of versions are the
|
||||
// same and we need to abort if actual version is > than expected
|
||||
let actual = SimpleRuntimeVersion::from_runtime_version(&env.runtime_version().await?);
|
||||
match actual.spec_version.cmp(&expected.spec_version) {
|
||||
Ordering::Less =>
|
||||
Err(Error::WaitingForRuntimeUpgrade { chain: C::NAME.into(), expected, actual }),
|
||||
Ordering::Equal => Ok(()),
|
||||
Ordering::Greater => {
|
||||
tracing::error!(
|
||||
target: "bridge",
|
||||
node=%C::NAME,
|
||||
?expected,
|
||||
?actual,
|
||||
"The client is configured to use runtime version, which is different from the \
|
||||
actual version. Aborting",
|
||||
);
|
||||
env.abort().await;
|
||||
Err(Error::Custom("Aborted".into()))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Build client to use in connection.
|
||||
async fn build_client(
|
||||
params: &ConnectionParams,
|
||||
) -> Result<(Arc<tokio::runtime::Runtime>, Arc<WsClient>)> {
|
||||
let tokio = tokio::runtime::Runtime::new()?;
|
||||
let uri = params.uri.clone();
|
||||
tracing::info!(target: "bridge", node=%C::NAME, %uri, "Connecting");
|
||||
|
||||
let client = tokio
|
||||
.spawn(async move {
|
||||
WsClientBuilder::default()
|
||||
.max_buffer_capacity_per_subscription(MAX_SUBSCRIPTION_CAPACITY)
|
||||
.build(&uri)
|
||||
.await
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok((Arc::new(tokio), Arc::new(client)))
|
||||
}
|
||||
|
||||
/// Execute jsonrpsee future in tokio context.
|
||||
async fn jsonrpsee_execute<MF, F, T>(&self, make_jsonrpsee_future: MF) -> Result<T>
|
||||
where
|
||||
MF: FnOnce(Arc<WsClient>) -> F + Send + 'static,
|
||||
F: Future<Output = Result<T>> + Send + 'static,
|
||||
T: Send + 'static,
|
||||
{
|
||||
let data = self.data.read().await;
|
||||
let client = data.client.clone();
|
||||
data.tokio.spawn(make_jsonrpsee_future(client)).await?
|
||||
}
|
||||
|
||||
/// Prepare parameters used to sign chain transactions.
|
||||
async fn build_sign_params(&self, signer: AccountKeyPairOf<C>) -> Result<SignParam<C>>
|
||||
where
|
||||
C: ChainWithTransactions,
|
||||
{
|
||||
let runtime_version = self.simple_runtime_version().await?;
|
||||
Ok(SignParam::<C> {
|
||||
spec_version: runtime_version.spec_version,
|
||||
transaction_version: runtime_version.transaction_version,
|
||||
genesis_hash: self.genesis_hash,
|
||||
signer,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the nonce of the given Bizinikiwi account.
|
||||
pub async fn next_account_index(&self, account: AccountIdOf<C>) -> Result<NonceOf<C>> {
|
||||
self.jsonrpsee_execute(move |client| async move {
|
||||
Ok(BizinikiwiFrameSystemClient::<C>::account_next_index(&*client, account).await?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Subscribe to finality justifications.
|
||||
async fn subscribe_finality_justifications<Fut>(
|
||||
&self,
|
||||
gadget_name: &str,
|
||||
do_subscribe: impl FnOnce(Arc<WsClient>) -> Fut + Send + 'static,
|
||||
) -> Result<Subscription<Bytes>>
|
||||
where
|
||||
Fut: Future<Output = std::result::Result<RpcSubscription<Bytes>, ClientError>> + Send,
|
||||
{
|
||||
let subscription = self
|
||||
.jsonrpsee_execute(move |client| async move { Ok(do_subscribe(client).await?) })
|
||||
.map_err(|e| Error::failed_to_subscribe_justification::<C>(e))
|
||||
.await?;
|
||||
|
||||
Ok(Subscription::new_forwarded(
|
||||
StreamDescription::new(format!("{} justifications", gadget_name), C::NAME.into()),
|
||||
subscription,
|
||||
))
|
||||
}
|
||||
|
||||
/// Subscribe to headers stream.
|
||||
async fn subscribe_headers<Fut>(
|
||||
&self,
|
||||
stream_name: &str,
|
||||
do_subscribe: impl FnOnce(Arc<WsClient>) -> Fut + Send + 'static,
|
||||
map_err: impl FnOnce(Error) -> Error,
|
||||
) -> Result<Subscription<HeaderOf<C>>>
|
||||
where
|
||||
Fut: Future<Output = std::result::Result<RpcSubscription<HeaderOf<C>>, ClientError>> + Send,
|
||||
{
|
||||
let subscription = self
|
||||
.jsonrpsee_execute(move |client| async move { Ok(do_subscribe(client).await?) })
|
||||
.map_err(map_err)
|
||||
.await?;
|
||||
|
||||
Ok(Subscription::new_forwarded(
|
||||
StreamDescription::new(format!("{} headers", stream_name), C::NAME.into()),
|
||||
subscription,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chain> Clone for RpcClient<C> {
|
||||
fn clone(&self) -> Self {
|
||||
RpcClient {
|
||||
params: self.params.clone(),
|
||||
submit_signed_extrinsic_lock: self.submit_signed_extrinsic_lock.clone(),
|
||||
genesis_hash: self.genesis_hash,
|
||||
data: self.data.clone(),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: Chain> Client<C> for RpcClient<C> {
|
||||
async fn ensure_synced(&self) -> Result<()> {
|
||||
let health = self
|
||||
.jsonrpsee_execute(|client| async move {
|
||||
Ok(BizinikiwiSystemClient::<C>::health(&*client).await?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_get_system_health::<C>(e))?;
|
||||
|
||||
let is_synced = !health.is_syncing && (!health.should_have_peers || health.peers > 0);
|
||||
if is_synced {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::ClientNotSynced(health))
|
||||
}
|
||||
}
|
||||
|
||||
async fn reconnect(&self) -> Result<()> {
|
||||
let mut data = self.data.write().await;
|
||||
let (tokio, client) = Self::build_client(&self.params).await?;
|
||||
data.tokio = tokio;
|
||||
data.client = client;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn genesis_hash(&self) -> HashOf<C> {
|
||||
self.genesis_hash
|
||||
}
|
||||
|
||||
async fn header_hash_by_number(&self, number: BlockNumberOf<C>) -> Result<HashOf<C>> {
|
||||
self.jsonrpsee_execute(move |client| async move {
|
||||
Ok(BizinikiwiChainClient::<C>::block_hash(&*client, Some(number)).await?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_read_header_hash_by_number::<C>(number, e))
|
||||
}
|
||||
|
||||
async fn header_by_hash(&self, hash: HashOf<C>) -> Result<HeaderOf<C>> {
|
||||
self.jsonrpsee_execute(move |client| async move {
|
||||
Ok(BizinikiwiChainClient::<C>::header(&*client, Some(hash)).await?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_read_header_by_hash::<C>(hash, e))
|
||||
}
|
||||
|
||||
async fn block_by_hash(&self, hash: HashOf<C>) -> Result<SignedBlockOf<C>> {
|
||||
self.jsonrpsee_execute(move |client| async move {
|
||||
Ok(BizinikiwiChainClient::<C>::block(&*client, Some(hash)).await?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_read_block_by_hash::<C>(hash, e))
|
||||
}
|
||||
|
||||
async fn best_finalized_header_hash(&self) -> Result<HashOf<C>> {
|
||||
self.jsonrpsee_execute(|client| async move {
|
||||
Ok(BizinikiwiChainClient::<C>::finalized_head(&*client).await?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_read_best_finalized_header_hash::<C>(e))
|
||||
}
|
||||
|
||||
async fn best_header(&self) -> Result<HeaderOf<C>> {
|
||||
self.jsonrpsee_execute(|client| async move {
|
||||
Ok(BizinikiwiChainClient::<C>::header(&*client, None).await?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_read_best_header::<C>(e))
|
||||
}
|
||||
|
||||
async fn subscribe_best_headers(&self) -> Result<Subscription<HeaderOf<C>>> {
|
||||
self.subscribe_headers(
|
||||
"best headers",
|
||||
move |client| async move { BizinikiwiChainClient::<C>::subscribe_new_heads(&*client).await },
|
||||
|e| Error::failed_to_subscribe_best_headers::<C>(e),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn subscribe_finalized_headers(&self) -> Result<Subscription<HeaderOf<C>>> {
|
||||
self.subscribe_headers(
|
||||
"best finalized headers",
|
||||
move |client| async move {
|
||||
BizinikiwiChainClient::<C>::subscribe_finalized_heads(&*client).await
|
||||
},
|
||||
|e| Error::failed_to_subscribe_finalized_headers::<C>(e),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn subscribe_grandpa_finality_justifications(&self) -> Result<Subscription<Bytes>>
|
||||
where
|
||||
C: ChainWithGrandpa,
|
||||
{
|
||||
self.subscribe_finality_justifications("GRANDPA", move |client| async move {
|
||||
BizinikiwiGrandpaClient::<C>::subscribe_justifications(&*client).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn generate_grandpa_key_ownership_proof(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
set_id: pezsp_consensus_grandpa::SetId,
|
||||
authority_id: pezsp_consensus_grandpa::AuthorityId,
|
||||
) -> Result<Option<pezsp_consensus_grandpa::OpaqueKeyOwnershipProof>> {
|
||||
self.state_call(
|
||||
at,
|
||||
SUB_API_GRANDPA_GENERATE_KEY_OWNERSHIP_PROOF.into(),
|
||||
(set_id, authority_id),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn subscribe_beefy_finality_justifications(&self) -> Result<Subscription<Bytes>> {
|
||||
self.subscribe_finality_justifications("BEEFY", move |client| async move {
|
||||
BizinikiwiBeefyClient::<C>::subscribe_justifications(&*client).await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn token_decimals(&self) -> Result<Option<u64>> {
|
||||
self.jsonrpsee_execute(move |client| async move {
|
||||
let system_properties = BizinikiwiSystemClient::<C>::properties(&*client).await?;
|
||||
Ok(system_properties.get("tokenDecimals").and_then(|v| v.as_u64()))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn runtime_version(&self) -> Result<RuntimeVersion> {
|
||||
self.jsonrpsee_execute(move |client| async move {
|
||||
Ok(BizinikiwiStateClient::<C>::runtime_version(&*client).await?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_read_runtime_version::<C>(e))
|
||||
}
|
||||
|
||||
async fn simple_runtime_version(&self) -> Result<SimpleRuntimeVersion> {
|
||||
Ok(match self.params.chain_runtime_version {
|
||||
ChainRuntimeVersion::Auto => {
|
||||
let runtime_version = self.runtime_version().await?;
|
||||
SimpleRuntimeVersion::from_runtime_version(&runtime_version)
|
||||
},
|
||||
ChainRuntimeVersion::Custom(ref version) => *version,
|
||||
})
|
||||
}
|
||||
|
||||
fn can_start_version_guard(&self) -> bool {
|
||||
!matches!(self.params.chain_runtime_version, ChainRuntimeVersion::Auto)
|
||||
}
|
||||
|
||||
async fn raw_storage_value(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
storage_key: StorageKey,
|
||||
) -> Result<Option<StorageData>> {
|
||||
let cloned_storage_key = storage_key.clone();
|
||||
self.jsonrpsee_execute(move |client| async move {
|
||||
Ok(BizinikiwiStateClient::<C>::storage(&*client, cloned_storage_key, Some(at)).await?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_read_storage_value::<C>(at, storage_key, e))
|
||||
}
|
||||
|
||||
async fn pending_extrinsics(&self) -> Result<Vec<Bytes>> {
|
||||
self.jsonrpsee_execute(move |client| async move {
|
||||
Ok(BizinikiwiAuthorClient::<C>::pending_extrinsics(&*client).await?)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_get_pending_extrinsics::<C>(e))
|
||||
}
|
||||
|
||||
async fn submit_unsigned_extrinsic(&self, transaction: Bytes) -> Result<HashOf<C>> {
|
||||
// one last check that the transaction is valid. Most of checks happen in the relay loop and
|
||||
// it is the "final" check before submission.
|
||||
let best_header_hash = self.best_header_hash().await?;
|
||||
self.validate_transaction(best_header_hash, PreEncoded(transaction.0.clone()))
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_submit_transaction::<C>(e))?
|
||||
.map_err(|e| Error::failed_to_submit_transaction::<C>(Error::TransactionInvalid(e)))?;
|
||||
|
||||
self.jsonrpsee_execute(move |client| async move {
|
||||
let tx_hash = BizinikiwiAuthorClient::<C>::submit_extrinsic(&*client, transaction)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(target: "bridge", error=?e, node=%C::NAME, "Failed to send transaction");
|
||||
e
|
||||
})?;
|
||||
tracing::trace!(target: "bridge", node=%C::NAME, ?tx_hash, "Sent transaction");
|
||||
Ok(tx_hash)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_submit_transaction::<C>(e))
|
||||
}
|
||||
|
||||
async fn submit_signed_extrinsic(
|
||||
&self,
|
||||
signer: &AccountKeyPairOf<C>,
|
||||
prepare_extrinsic: impl FnOnce(HeaderIdOf<C>, NonceOf<C>) -> Result<UnsignedTransaction<C>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
) -> Result<HashOf<C>>
|
||||
where
|
||||
C: ChainWithTransactions,
|
||||
AccountIdOf<C>: From<<AccountKeyPairOf<C> as Pair>::Public>,
|
||||
{
|
||||
let _guard = self.submit_signed_extrinsic_lock.lock().await;
|
||||
let transaction_nonce = self.next_account_index(signer.public().into()).await?;
|
||||
let best_header = self.best_header().await?;
|
||||
let signing_data = self.build_sign_params(signer.clone()).await?;
|
||||
|
||||
// By using parent of best block here, we are protecting again best-block reorganizations.
|
||||
// E.g. transaction may have been submitted when the best block was `A[num=100]`. Then it
|
||||
// has been changed to `B[num=100]`. Hash of `A` has been included into transaction
|
||||
// signature payload. So when signature will be checked, the check will fail and transaction
|
||||
// will be dropped from the pool.
|
||||
let best_header_id = best_header.parent_id().unwrap_or_else(|| best_header.id());
|
||||
|
||||
let extrinsic = prepare_extrinsic(best_header_id, transaction_nonce)?;
|
||||
let signed_extrinsic = C::sign_transaction(signing_data, extrinsic)?.encode();
|
||||
self.submit_unsigned_extrinsic(Bytes(signed_extrinsic)).await
|
||||
}
|
||||
|
||||
async fn submit_and_watch_signed_extrinsic(
|
||||
&self,
|
||||
signer: &AccountKeyPairOf<C>,
|
||||
prepare_extrinsic: impl FnOnce(HeaderIdOf<C>, NonceOf<C>) -> Result<UnsignedTransaction<C>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
) -> Result<TransactionTracker<C, Self>>
|
||||
where
|
||||
C: ChainWithTransactions,
|
||||
AccountIdOf<C>: From<<AccountKeyPairOf<C> as Pair>::Public>,
|
||||
{
|
||||
let self_clone = self.clone();
|
||||
let signing_data = self.build_sign_params(signer.clone()).await?;
|
||||
let _guard = self.submit_signed_extrinsic_lock.lock().await;
|
||||
let transaction_nonce = self.next_account_index(signer.public().into()).await?;
|
||||
let best_header = self.best_header().await?;
|
||||
let best_header_id = best_header.id();
|
||||
|
||||
let extrinsic = prepare_extrinsic(best_header_id, transaction_nonce)?;
|
||||
let stall_timeout = transaction_stall_timeout(
|
||||
extrinsic.era.mortality_period(),
|
||||
C::AVERAGE_BLOCK_INTERVAL,
|
||||
STALL_TIMEOUT,
|
||||
);
|
||||
let signed_extrinsic = C::sign_transaction(signing_data, extrinsic)?.encode();
|
||||
|
||||
// one last check that the transaction is valid. Most of checks happen in the relay loop and
|
||||
// it is the "final" check before submission.
|
||||
self.validate_transaction(best_header_id.hash(), PreEncoded(signed_extrinsic.clone()))
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_submit_transaction::<C>(e))?
|
||||
.map_err(|e| Error::failed_to_submit_transaction::<C>(Error::TransactionInvalid(e)))?;
|
||||
|
||||
self.jsonrpsee_execute(move |client| async move {
|
||||
let tx_hash = C::Hasher::hash(&signed_extrinsic);
|
||||
let subscription: jsonrpsee::core::client::Subscription<_> =
|
||||
BizinikiwiAuthorClient::<C>::submit_and_watch_extrinsic(
|
||||
&*client,
|
||||
Bytes(signed_extrinsic),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!(target: "bridge", error=?e, node=%C::NAME, "Failed to send transaction");
|
||||
e
|
||||
})?;
|
||||
tracing::trace!(target: "bridge", node=%C::NAME, ?tx_hash, "Sent transaction");
|
||||
Ok(TransactionTracker::new(
|
||||
self_clone,
|
||||
stall_timeout,
|
||||
tx_hash,
|
||||
Subscription::new_forwarded(
|
||||
StreamDescription::new("transaction events".into(), C::NAME.into()),
|
||||
subscription,
|
||||
),
|
||||
))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_submit_transaction::<C>(e))
|
||||
}
|
||||
|
||||
async fn validate_transaction<SignedTransaction: Encode + Send + 'static>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
transaction: SignedTransaction,
|
||||
) -> Result<TransactionValidity> {
|
||||
self.state_call(
|
||||
at,
|
||||
SUB_API_TXPOOL_VALIDATE_TRANSACTION.into(),
|
||||
(TransactionSource::External, transaction, at),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn estimate_extrinsic_weight<SignedTransaction: Encode + Send + 'static>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
transaction: SignedTransaction,
|
||||
) -> Result<Weight> {
|
||||
let transaction_len = transaction.encoded_size() as u32;
|
||||
let dispatch_info: RuntimeDispatchInfo<BalanceOf<C>> = self
|
||||
.state_call(at, SUB_API_TX_PAYMENT_QUERY_INFO.into(), (transaction, transaction_len))
|
||||
.await?;
|
||||
|
||||
Ok(dispatch_info.weight)
|
||||
}
|
||||
|
||||
async fn raw_state_call<Args: Encode + Send>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
method: String,
|
||||
arguments: Args,
|
||||
) -> Result<Bytes> {
|
||||
let arguments = Bytes(arguments.encode());
|
||||
let arguments_clone = arguments.clone();
|
||||
let method_clone = method.clone();
|
||||
self.jsonrpsee_execute(move |client| async move {
|
||||
BizinikiwiStateClient::<C>::call(&*client, method, arguments, Some(at))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_state_call::<C>(at, method_clone, arguments_clone, e))
|
||||
}
|
||||
|
||||
async fn prove_storage(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
keys: Vec<StorageKey>,
|
||||
) -> Result<(StorageProof, HashOf<C>)> {
|
||||
let state_root = *self.header_by_hash(at).await?.state_root();
|
||||
|
||||
let keys_clone = keys.clone();
|
||||
let read_proof = self
|
||||
.jsonrpsee_execute(move |client| async move {
|
||||
BizinikiwiStateClient::<C>::prove_storage(&*client, keys_clone, Some(at))
|
||||
.await
|
||||
.map(|proof| StorageProof::new(proof.proof.into_iter().map(|b| b.0)))
|
||||
.map_err(Into::into)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| Error::failed_to_prove_storage::<C>(at, keys.clone(), e))?;
|
||||
|
||||
Ok((read_proof, state_root))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{guard::tests::TestEnvironment, test_chain::TestChain};
|
||||
use futures::{channel::mpsc::unbounded, FutureExt, SinkExt, StreamExt};
|
||||
|
||||
async fn run_ensure_correct_runtime_version(
|
||||
expected: ChainRuntimeVersion,
|
||||
actual: RuntimeVersion,
|
||||
) -> Result<()> {
|
||||
let (
|
||||
(mut runtime_version_tx, runtime_version_rx),
|
||||
(slept_tx, _slept_rx),
|
||||
(aborted_tx, mut aborted_rx),
|
||||
) = (unbounded(), unbounded(), unbounded());
|
||||
runtime_version_tx.send(actual).await.unwrap();
|
||||
let mut env = TestEnvironment { runtime_version_rx, slept_tx, aborted_tx };
|
||||
|
||||
let ensure_correct_runtime_version =
|
||||
RpcClient::<TestChain>::ensure_correct_runtime_version(&mut env, expected).boxed();
|
||||
let aborted = aborted_rx.next().map(|_| Err(Error::Custom("".into()))).boxed();
|
||||
futures::pin_mut!(ensure_correct_runtime_version, aborted);
|
||||
futures::future::select(ensure_correct_runtime_version, aborted)
|
||||
.await
|
||||
.into_inner()
|
||||
.0
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn ensure_correct_runtime_version_works() {
|
||||
// when we are configured to use auto version
|
||||
assert!(matches!(
|
||||
run_ensure_correct_runtime_version(
|
||||
ChainRuntimeVersion::Auto,
|
||||
RuntimeVersion {
|
||||
spec_version: 100,
|
||||
transaction_version: 100,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await,
|
||||
Ok(()),
|
||||
));
|
||||
// when actual == expected
|
||||
assert!(matches!(
|
||||
run_ensure_correct_runtime_version(
|
||||
ChainRuntimeVersion::Custom(SimpleRuntimeVersion {
|
||||
spec_version: 100,
|
||||
transaction_version: 100
|
||||
}),
|
||||
RuntimeVersion {
|
||||
spec_version: 100,
|
||||
transaction_version: 100,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await,
|
||||
Ok(()),
|
||||
));
|
||||
// when actual spec version < expected spec version
|
||||
assert!(matches!(
|
||||
run_ensure_correct_runtime_version(
|
||||
ChainRuntimeVersion::Custom(SimpleRuntimeVersion {
|
||||
spec_version: 100,
|
||||
transaction_version: 100
|
||||
}),
|
||||
RuntimeVersion { spec_version: 99, transaction_version: 100, ..Default::default() },
|
||||
)
|
||||
.await,
|
||||
Err(Error::WaitingForRuntimeUpgrade {
|
||||
expected: SimpleRuntimeVersion { spec_version: 100, transaction_version: 100 },
|
||||
actual: SimpleRuntimeVersion { spec_version: 99, transaction_version: 100 },
|
||||
..
|
||||
}),
|
||||
));
|
||||
// when actual spec version > expected spec version
|
||||
assert!(matches!(
|
||||
run_ensure_correct_runtime_version(
|
||||
ChainRuntimeVersion::Custom(SimpleRuntimeVersion {
|
||||
spec_version: 100,
|
||||
transaction_version: 100
|
||||
}),
|
||||
RuntimeVersion {
|
||||
spec_version: 101,
|
||||
transaction_version: 100,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await,
|
||||
Err(Error::Custom(_)),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! The most generic Bizinikiwi node RPC interface.
|
||||
|
||||
use crate::{Chain, ChainWithGrandpa, TransactionStatusOf};
|
||||
|
||||
use jsonrpsee::proc_macros::rpc;
|
||||
use pezpallet_transaction_payment_rpc_runtime_api::FeeDetails;
|
||||
use pezsc_rpc_api::{state::ReadProof, system::Health};
|
||||
use pezsp_core::{
|
||||
storage::{StorageData, StorageKey},
|
||||
Bytes,
|
||||
};
|
||||
use pezsp_rpc::number::NumberOrHex;
|
||||
use pezsp_version::RuntimeVersion;
|
||||
|
||||
/// RPC methods of Bizinikiwi `system` namespace, that we are using.
|
||||
#[rpc(client, client_bounds(C: Chain), namespace = "system")]
|
||||
pub(crate) trait BizinikiwiSystem<C> {
|
||||
/// Return node health.
|
||||
#[method(name = "health")]
|
||||
async fn health(&self) -> RpcResult<Health>;
|
||||
/// Return system properties.
|
||||
#[method(name = "properties")]
|
||||
async fn properties(&self) -> RpcResult<pezsc_chain_spec::Properties>;
|
||||
}
|
||||
|
||||
/// RPC methods of Bizinikiwi `chain` namespace, that we are using.
|
||||
#[rpc(client, client_bounds(C: Chain), namespace = "chain")]
|
||||
pub(crate) trait BizinikiwiChain<C> {
|
||||
/// Get block hash by its number.
|
||||
#[method(name = "getBlockHash")]
|
||||
async fn block_hash(&self, block_number: Option<C::BlockNumber>) -> RpcResult<C::Hash>;
|
||||
/// Return block header by its hash.
|
||||
#[method(name = "getHeader")]
|
||||
async fn header(&self, block_hash: Option<C::Hash>) -> RpcResult<C::Header>;
|
||||
/// Return best finalized block hash.
|
||||
#[method(name = "getFinalizedHead")]
|
||||
async fn finalized_head(&self) -> RpcResult<C::Hash>;
|
||||
/// Return signed block (with justifications) by its hash.
|
||||
#[method(name = "getBlock")]
|
||||
async fn block(&self, block_hash: Option<C::Hash>) -> RpcResult<C::SignedBlock>;
|
||||
/// Subscribe to best headers.
|
||||
#[subscription(
|
||||
name = "subscribeNewHeads" => "newHead",
|
||||
unsubscribe = "unsubscribeNewHeads",
|
||||
item = C::Header
|
||||
)]
|
||||
async fn subscribe_new_heads(&self);
|
||||
/// Subscribe to finalized headers.
|
||||
#[subscription(
|
||||
name = "subscribeFinalizedHeads" => "finalizedHead",
|
||||
unsubscribe = "unsubscribeFinalizedHeads",
|
||||
item = C::Header
|
||||
)]
|
||||
async fn subscribe_finalized_heads(&self);
|
||||
}
|
||||
|
||||
/// RPC methods of Bizinikiwi `author` namespace, that we are using.
|
||||
#[rpc(client, client_bounds(C: Chain), namespace = "author")]
|
||||
pub(crate) trait BizinikiwiAuthor<C> {
|
||||
/// Submit extrinsic to the transaction pool.
|
||||
#[method(name = "submitExtrinsic")]
|
||||
async fn submit_extrinsic(&self, extrinsic: Bytes) -> RpcResult<C::Hash>;
|
||||
/// Return vector of pending extrinsics from the transaction pool.
|
||||
#[method(name = "pendingExtrinsics")]
|
||||
async fn pending_extrinsics(&self) -> RpcResult<Vec<Bytes>>;
|
||||
/// Submit and watch for extrinsic state.
|
||||
#[subscription(name = "submitAndWatchExtrinsic", unsubscribe = "unwatchExtrinsic", item = TransactionStatusOf<C>)]
|
||||
async fn submit_and_watch_extrinsic(&self, extrinsic: Bytes);
|
||||
}
|
||||
|
||||
/// RPC methods of Bizinikiwi `state` namespace, that we are using.
|
||||
#[rpc(client, client_bounds(C: Chain), namespace = "state")]
|
||||
pub(crate) trait BizinikiwiState<C> {
|
||||
/// Get current runtime version.
|
||||
#[method(name = "getRuntimeVersion")]
|
||||
async fn runtime_version(&self) -> RpcResult<RuntimeVersion>;
|
||||
/// Call given runtime method.
|
||||
#[method(name = "call")]
|
||||
async fn call(
|
||||
&self,
|
||||
method: String,
|
||||
data: Bytes,
|
||||
at_block: Option<C::Hash>,
|
||||
) -> RpcResult<Bytes>;
|
||||
/// Get value of the runtime storage.
|
||||
#[method(name = "getStorage")]
|
||||
async fn storage(
|
||||
&self,
|
||||
key: StorageKey,
|
||||
at_block: Option<C::Hash>,
|
||||
) -> RpcResult<Option<StorageData>>;
|
||||
/// Get proof of the runtime storage value.
|
||||
#[method(name = "getReadProof")]
|
||||
async fn prove_storage(
|
||||
&self,
|
||||
keys: Vec<StorageKey>,
|
||||
hash: Option<C::Hash>,
|
||||
) -> RpcResult<ReadProof<C::Hash>>;
|
||||
}
|
||||
|
||||
/// RPC methods of Bizinikiwi `grandpa` namespace, that we are using.
|
||||
#[rpc(client, client_bounds(C: ChainWithGrandpa), namespace = "grandpa")]
|
||||
pub(crate) trait BizinikiwiGrandpa<C> {
|
||||
/// Subscribe to GRANDPA justifications.
|
||||
#[subscription(name = "subscribeJustifications", unsubscribe = "unsubscribeJustifications", item = Bytes)]
|
||||
async fn subscribe_justifications(&self);
|
||||
}
|
||||
|
||||
// TODO: Use `ChainWithBeefy` instead of `Chain` after #1606 is merged
|
||||
/// RPC methods of Bizinikiwi `beefy` namespace, that we are using.
|
||||
#[rpc(client, client_bounds(C: Chain), namespace = "beefy")]
|
||||
pub(crate) trait BizinikiwiBeefy<C> {
|
||||
/// Subscribe to BEEFY justifications.
|
||||
#[subscription(name = "subscribeJustifications", unsubscribe = "unsubscribeJustifications", item = Bytes)]
|
||||
async fn subscribe_justifications(&self);
|
||||
}
|
||||
|
||||
/// RPC methods of Bizinikiwi `system` frame pezpallet, that we are using.
|
||||
#[rpc(client, client_bounds(C: Chain), namespace = "system")]
|
||||
pub(crate) trait BizinikiwiFrameSystem<C> {
|
||||
/// Return index of next account transaction.
|
||||
#[method(name = "accountNextIndex")]
|
||||
async fn account_next_index(&self, account_id: C::AccountId) -> RpcResult<C::Nonce>;
|
||||
}
|
||||
|
||||
/// RPC methods of Bizinikiwi `pezpallet_transaction_payment` frame pezpallet, that we are using.
|
||||
#[rpc(client, client_bounds(C: Chain), namespace = "payment")]
|
||||
pub(crate) trait BizinikiwiTransactionPayment<C> {
|
||||
/// Query transaction fee details.
|
||||
#[method(name = "queryFeeDetails")]
|
||||
async fn fee_details(
|
||||
&self,
|
||||
extrinsic: Bytes,
|
||||
at_block: Option<C::Hash>,
|
||||
) -> RpcResult<FeeDetails<NumberOrHex>>;
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::error::Result as ClientResult;
|
||||
|
||||
use async_std::{
|
||||
channel::{bounded, Receiver, Sender},
|
||||
stream::StreamExt,
|
||||
};
|
||||
use futures::{FutureExt, Stream};
|
||||
use pezsp_runtime::DeserializeOwned;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
pin::Pin,
|
||||
result::Result as StdResult,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
/// Once channel reaches this capacity, the subscription breaks.
|
||||
const CHANNEL_CAPACITY: usize = 128;
|
||||
|
||||
/// Structure describing a stream.
|
||||
#[derive(Clone)]
|
||||
pub struct StreamDescription {
|
||||
stream_name: String,
|
||||
chain_name: String,
|
||||
}
|
||||
|
||||
impl StreamDescription {
|
||||
/// Create a new instance of `StreamDescription`.
|
||||
pub fn new(stream_name: String, chain_name: String) -> Self {
|
||||
Self { stream_name, chain_name }
|
||||
}
|
||||
|
||||
/// Get a stream description.
|
||||
fn get(&self) -> String {
|
||||
format!("{} stream of {}", self.stream_name, self.chain_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Chainable stream that transforms items of type `Result<T, E>` to items of type `T`.
|
||||
///
|
||||
/// If it encounters an item of type `Err`, it returns `Poll::Ready(None)`
|
||||
/// and terminates the underlying stream.
|
||||
struct Unwrap<S: Stream<Item = StdResult<T, E>>, T, E> {
|
||||
desc: StreamDescription,
|
||||
stream: Option<S>,
|
||||
}
|
||||
|
||||
impl<S: Stream<Item = StdResult<T, E>>, T, E> Unwrap<S, T, E> {
|
||||
/// Create a new instance of `Unwrap`.
|
||||
pub fn new(desc: StreamDescription, stream: S) -> Self {
|
||||
Self { desc, stream: Some(stream) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Stream<Item = StdResult<T, E>> + Unpin, T: DeserializeOwned, E: Debug> Stream
|
||||
for Unwrap<S, T, E>
|
||||
{
|
||||
type Item = T;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Poll::Ready(match self.stream.as_mut() {
|
||||
Some(subscription) => match futures::ready!(Pin::new(subscription).poll_next(cx)) {
|
||||
Some(Ok(item)) => Some(item),
|
||||
Some(Err(e)) => {
|
||||
self.stream.take();
|
||||
tracing::debug!(
|
||||
target: "bridge",
|
||||
error=?e,
|
||||
desc=%self.desc.get(),
|
||||
"Returned with error. It may need to be restarted"
|
||||
);
|
||||
None
|
||||
},
|
||||
None => {
|
||||
self.stream.take();
|
||||
tracing::debug!(
|
||||
target: "bridge",
|
||||
desc=%self.desc.get(),
|
||||
"Returned `None`. It may need to be restarted"
|
||||
);
|
||||
None
|
||||
},
|
||||
},
|
||||
None => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscription factory that produces subscriptions, sharing the same background thread.
|
||||
#[derive(Clone)]
|
||||
pub struct SubscriptionBroadcaster<T> {
|
||||
desc: StreamDescription,
|
||||
subscribers_sender: Sender<Sender<T>>,
|
||||
}
|
||||
|
||||
impl<T: 'static + Clone + DeserializeOwned + Send> SubscriptionBroadcaster<T> {
|
||||
/// Create new subscription factory.
|
||||
pub fn new(subscription: Subscription<T>) -> StdResult<Self, Subscription<T>> {
|
||||
// It doesn't make sense to further broadcast a broadcasted subscription.
|
||||
if subscription.is_broadcasted {
|
||||
return Err(subscription);
|
||||
}
|
||||
|
||||
let desc = subscription.desc().clone();
|
||||
let (subscribers_sender, subscribers_receiver) = bounded(CHANNEL_CAPACITY);
|
||||
async_std::task::spawn(background_worker(subscription, subscribers_receiver));
|
||||
Ok(Self { desc, subscribers_sender })
|
||||
}
|
||||
|
||||
/// Produce new subscription.
|
||||
pub async fn subscribe(&self) -> ClientResult<Subscription<T>> {
|
||||
let (items_sender, items_receiver) = bounded(CHANNEL_CAPACITY);
|
||||
self.subscribers_sender.try_send(items_sender)?;
|
||||
|
||||
Ok(Subscription::new_broadcasted(self.desc.clone(), items_receiver))
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscription to some chain events.
|
||||
pub struct Subscription<T> {
|
||||
desc: StreamDescription,
|
||||
subscription: Box<dyn Stream<Item = T> + Unpin + Send>,
|
||||
is_broadcasted: bool,
|
||||
}
|
||||
|
||||
impl<T: 'static + Clone + DeserializeOwned + Send> Subscription<T> {
|
||||
/// Create new forwarded subscription.
|
||||
pub fn new_forwarded(
|
||||
desc: StreamDescription,
|
||||
subscription: impl Stream<Item = StdResult<T, serde_json::Error>> + Unpin + Send + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
desc: desc.clone(),
|
||||
subscription: Box::new(Unwrap::new(desc, subscription)),
|
||||
is_broadcasted: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new broadcasted subscription.
|
||||
pub fn new_broadcasted(
|
||||
desc: StreamDescription,
|
||||
subscription: impl Stream<Item = T> + Unpin + Send + 'static,
|
||||
) -> Self {
|
||||
Self { desc, subscription: Box::new(subscription), is_broadcasted: true }
|
||||
}
|
||||
|
||||
/// Get the description of the underlying stream
|
||||
pub fn desc(&self) -> &StreamDescription {
|
||||
&self.desc
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Stream for Subscription<T> {
|
||||
type Item = T;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Poll::Ready(futures::ready!(Pin::new(&mut self.subscription).poll_next(cx)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Background worker that is executed in tokio context as `jsonrpsee` requires.
|
||||
///
|
||||
/// This task may exit under some circumstances. It'll send the correspondent
|
||||
/// message (`Err` or `None`) to all known listeners. Also, when it stops, all
|
||||
/// subsequent reads and new subscribers will get the connection error (`ChannelError`).
|
||||
async fn background_worker<T: 'static + Clone + DeserializeOwned + Send>(
|
||||
mut subscription: Subscription<T>,
|
||||
mut subscribers_receiver: Receiver<Sender<T>>,
|
||||
) {
|
||||
fn log_task_exit(desc: &StreamDescription, reason: &str) {
|
||||
tracing::debug!(
|
||||
target: "bridge",
|
||||
desc=%desc.get(),
|
||||
%reason,
|
||||
"Background task of subscription broadcaster has stopped"
|
||||
);
|
||||
}
|
||||
|
||||
// wait for first subscriber until actually starting subscription
|
||||
let subscriber = match subscribers_receiver.next().await {
|
||||
Some(subscriber) => subscriber,
|
||||
None => {
|
||||
// it means that the last subscriber/factory has been dropped, so we need to
|
||||
// exit too
|
||||
return log_task_exit(subscription.desc(), "client has stopped");
|
||||
},
|
||||
};
|
||||
|
||||
// actually subscribe
|
||||
let mut subscribers = vec![subscriber];
|
||||
|
||||
// start listening for new items and receivers
|
||||
loop {
|
||||
futures::select! {
|
||||
subscriber = subscribers_receiver.next().fuse() => {
|
||||
match subscriber {
|
||||
Some(subscriber) => subscribers.push(subscriber),
|
||||
None => {
|
||||
// it means that the last subscriber/factory has been dropped, so we need to
|
||||
// exit too
|
||||
return log_task_exit(subscription.desc(), "client has stopped")
|
||||
},
|
||||
}
|
||||
},
|
||||
maybe_item = subscription.subscription.next().fuse() => {
|
||||
match maybe_item {
|
||||
Some(item) => {
|
||||
// notify subscribers
|
||||
subscribers.retain(|subscriber| {
|
||||
let send_result = subscriber.try_send(item.clone());
|
||||
send_result.is_ok()
|
||||
});
|
||||
}
|
||||
None => {
|
||||
// The underlying client has dropped, so we can't do anything here
|
||||
// and need to stop the task.
|
||||
return log_task_exit(subscription.desc(), "stream has finished");
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{
|
||||
error::{Error, Result},
|
||||
AccountIdOf, AccountKeyPairOf, BlockNumberOf, Chain, ChainWithGrandpa, ChainWithTransactions,
|
||||
HashOf, HeaderIdOf, HeaderOf, NonceOf, SignedBlockOf, SimpleRuntimeVersion, Subscription,
|
||||
TransactionTracker, UnsignedTransaction,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use pezbp_runtime::{StorageDoubleMapKeyProvider, StorageMapKeyProvider};
|
||||
use codec::{Decode, Encode};
|
||||
use pezframe_support::weights::Weight;
|
||||
use pezsp_core::{
|
||||
storage::{StorageData, StorageKey},
|
||||
Bytes, Pair,
|
||||
};
|
||||
use pezsp_runtime::{traits::Header as _, transaction_validity::TransactionValidity};
|
||||
use pezsp_trie::StorageProof;
|
||||
use pezsp_version::RuntimeVersion;
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Relay uses the `Client` to communicate with the node, connected to Bizinikiwi
|
||||
/// chain `C`.
|
||||
#[async_trait]
|
||||
pub trait Client<C: Chain>: 'static + Send + Sync + Clone + Debug {
|
||||
/// Returns error if client has no connected peers or it believes it is far
|
||||
/// behind the chain tip.
|
||||
async fn ensure_synced(&self) -> Result<()>;
|
||||
/// Reconnects the client.
|
||||
async fn reconnect(&self) -> Result<()>;
|
||||
|
||||
/// Return hash of the genesis block.
|
||||
fn genesis_hash(&self) -> HashOf<C>;
|
||||
/// Get header hash by number.
|
||||
async fn header_hash_by_number(&self, number: BlockNumberOf<C>) -> Result<HashOf<C>>;
|
||||
/// Get header by hash.
|
||||
async fn header_by_hash(&self, hash: HashOf<C>) -> Result<HeaderOf<C>>;
|
||||
/// Get header by number.
|
||||
async fn header_by_number(&self, number: BlockNumberOf<C>) -> Result<HeaderOf<C>> {
|
||||
self.header_by_hash(self.header_hash_by_number(number).await?).await
|
||||
}
|
||||
/// Get block by hash.
|
||||
async fn block_by_hash(&self, hash: HashOf<C>) -> Result<SignedBlockOf<C>>;
|
||||
|
||||
/// Get best finalized header hash.
|
||||
async fn best_finalized_header_hash(&self) -> Result<HashOf<C>>;
|
||||
/// Get best finalized header number.
|
||||
async fn best_finalized_header_number(&self) -> Result<BlockNumberOf<C>> {
|
||||
Ok(*self.best_finalized_header().await?.number())
|
||||
}
|
||||
/// Get best finalized header.
|
||||
async fn best_finalized_header(&self) -> Result<HeaderOf<C>> {
|
||||
self.header_by_hash(self.best_finalized_header_hash().await?).await
|
||||
}
|
||||
|
||||
/// Get best header.
|
||||
async fn best_header(&self) -> Result<HeaderOf<C>>;
|
||||
/// Get best header hash.
|
||||
async fn best_header_hash(&self) -> Result<HashOf<C>> {
|
||||
Ok(self.best_header().await?.hash())
|
||||
}
|
||||
|
||||
/// Subscribe to new best headers.
|
||||
async fn subscribe_best_headers(&self) -> Result<Subscription<HeaderOf<C>>>;
|
||||
/// Subscribe to new finalized headers.
|
||||
async fn subscribe_finalized_headers(&self) -> Result<Subscription<HeaderOf<C>>>;
|
||||
|
||||
/// Subscribe to GRANDPA finality justifications.
|
||||
async fn subscribe_grandpa_finality_justifications(&self) -> Result<Subscription<Bytes>>
|
||||
where
|
||||
C: ChainWithGrandpa;
|
||||
/// Generates a proof of key ownership for the given authority in the given set.
|
||||
async fn generate_grandpa_key_ownership_proof(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
set_id: pezsp_consensus_grandpa::SetId,
|
||||
authority_id: pezsp_consensus_grandpa::AuthorityId,
|
||||
) -> Result<Option<pezsp_consensus_grandpa::OpaqueKeyOwnershipProof>>;
|
||||
|
||||
/// Subscribe to BEEFY finality justifications.
|
||||
async fn subscribe_beefy_finality_justifications(&self) -> Result<Subscription<Bytes>>;
|
||||
|
||||
/// Return `tokenDecimals` property from the set of chain properties.
|
||||
async fn token_decimals(&self) -> Result<Option<u64>>;
|
||||
/// Get runtime version of the connected chain.
|
||||
async fn runtime_version(&self) -> Result<RuntimeVersion>;
|
||||
/// Get partial runtime version, to use when signing transactions.
|
||||
async fn simple_runtime_version(&self) -> Result<SimpleRuntimeVersion>;
|
||||
/// Returns `true` if version guard can be started.
|
||||
///
|
||||
/// There's no reason to run version guard when version mode is set to `Auto`. It can
|
||||
/// lead to relay shutdown when chain is upgraded, even though we have explicitly
|
||||
/// said that we don't want to shutdown.
|
||||
fn can_start_version_guard(&self) -> bool;
|
||||
|
||||
/// Read raw value from runtime storage.
|
||||
async fn raw_storage_value(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
storage_key: StorageKey,
|
||||
) -> Result<Option<StorageData>>;
|
||||
/// Read and decode value from runtime storage.
|
||||
async fn storage_value<T: Decode + 'static>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
storage_key: StorageKey,
|
||||
) -> Result<Option<T>> {
|
||||
self.raw_storage_value(at, storage_key.clone())
|
||||
.await?
|
||||
.map(|encoded_value| {
|
||||
T::decode(&mut &encoded_value.0[..]).map_err(|e| {
|
||||
Error::failed_to_read_storage_value::<C>(at, storage_key, e.into())
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
/// Read and decode value from runtime storage map.
|
||||
///
|
||||
/// `pezpallet_prefix` is the name of the pezpallet (used in `construct_runtime`), which
|
||||
/// "contains" the storage map.
|
||||
async fn storage_map_value<T: StorageMapKeyProvider>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
pezpallet_prefix: &str,
|
||||
storage_key: &T::Key,
|
||||
) -> Result<Option<T::Value>> {
|
||||
self.storage_value(at, T::final_key(pezpallet_prefix, storage_key)).await
|
||||
}
|
||||
/// Read and decode value from runtime storage double map.
|
||||
///
|
||||
/// `pezpallet_prefix` is the name of the pezpallet (used in `construct_runtime`), which
|
||||
/// "contains" the storage double map.
|
||||
async fn storage_double_map_value<T: StorageDoubleMapKeyProvider>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
pezpallet_prefix: &str,
|
||||
key1: &T::Key1,
|
||||
key2: &T::Key2,
|
||||
) -> Result<Option<T::Value>> {
|
||||
self.storage_value(at, T::final_key(pezpallet_prefix, key1, key2)).await
|
||||
}
|
||||
|
||||
/// Returns pending extrinsics from transaction pool.
|
||||
async fn pending_extrinsics(&self) -> Result<Vec<Bytes>>;
|
||||
/// Submit unsigned extrinsic for inclusion in a block.
|
||||
///
|
||||
/// Note: The given transaction needs to be SCALE encoded beforehand.
|
||||
async fn submit_unsigned_extrinsic(&self, transaction: Bytes) -> Result<HashOf<C>>;
|
||||
/// Submit an extrinsic signed by given account.
|
||||
///
|
||||
/// All calls of this method are synchronized, so there can't be more than one active
|
||||
/// `submit_signed_extrinsic()` call. This guarantees that no nonces collision may happen
|
||||
/// if all client instances are clones of the same initial `Client`.
|
||||
///
|
||||
/// Note: The given transaction needs to be SCALE encoded beforehand.
|
||||
async fn submit_signed_extrinsic(
|
||||
&self,
|
||||
signer: &AccountKeyPairOf<C>,
|
||||
prepare_extrinsic: impl FnOnce(HeaderIdOf<C>, NonceOf<C>) -> Result<UnsignedTransaction<C>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
) -> Result<HashOf<C>>
|
||||
where
|
||||
C: ChainWithTransactions,
|
||||
AccountIdOf<C>: From<<AccountKeyPairOf<C> as Pair>::Public>;
|
||||
/// Does exactly the same as `submit_signed_extrinsic`, but keeps watching for extrinsic status
|
||||
/// after submission.
|
||||
async fn submit_and_watch_signed_extrinsic(
|
||||
&self,
|
||||
signer: &AccountKeyPairOf<C>,
|
||||
prepare_extrinsic: impl FnOnce(HeaderIdOf<C>, NonceOf<C>) -> Result<UnsignedTransaction<C>>
|
||||
+ Send
|
||||
+ 'static,
|
||||
) -> Result<TransactionTracker<C, Self>>
|
||||
where
|
||||
C: ChainWithTransactions,
|
||||
AccountIdOf<C>: From<<AccountKeyPairOf<C> as Pair>::Public>;
|
||||
/// Validate transaction at given block.
|
||||
async fn validate_transaction<SignedTransaction: Encode + Send + 'static>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
transaction: SignedTransaction,
|
||||
) -> Result<TransactionValidity>;
|
||||
/// Returns weight of the given transaction.
|
||||
async fn estimate_extrinsic_weight<SignedTransaction: Encode + Send + 'static>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
transaction: SignedTransaction,
|
||||
) -> Result<Weight>;
|
||||
|
||||
/// Execute runtime call at given block.
|
||||
async fn raw_state_call<Args: Encode + Send>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
method: String,
|
||||
arguments: Args,
|
||||
) -> Result<Bytes>;
|
||||
/// Execute runtime call at given block, provided the input and output types.
|
||||
/// It also performs the input encode and output decode.
|
||||
async fn state_call<Args: Encode + Send, Ret: Decode>(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
method: String,
|
||||
arguments: Args,
|
||||
) -> Result<Ret> {
|
||||
let encoded_arguments = arguments.encode();
|
||||
let encoded_output = self.raw_state_call(at, method.clone(), arguments).await?;
|
||||
Ret::decode(&mut &encoded_output.0[..]).map_err(|e| {
|
||||
Error::failed_state_call::<C>(at, method, Bytes(encoded_arguments), e.into())
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns storage proof of given storage keys and state root.
|
||||
async fn prove_storage(
|
||||
&self,
|
||||
at: HashOf<C>,
|
||||
keys: Vec<StorageKey>,
|
||||
) -> Result<(StorageProof, HashOf<C>)>;
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Bizinikiwi node RPC errors.
|
||||
|
||||
use crate::{BlockNumberOf, Chain, HashOf, SimpleRuntimeVersion};
|
||||
use bp_header_pez_chain::SubmitFinalityProofCallExtras;
|
||||
use bp_pezkuwi_core::teyrchains::ParaId;
|
||||
use jsonrpsee::core::ClientError as RpcError;
|
||||
use relay_utils::MaybeConnectionError;
|
||||
use pezsc_rpc_api::system::Health;
|
||||
use pezsp_core::{storage::StorageKey, Bytes};
|
||||
use pezsp_runtime::transaction_validity::TransactionValidityError;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Result type used by Bizinikiwi client.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Errors that can occur only when interacting with
|
||||
/// a Bizinikiwi node through RPC.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
/// IO error.
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
/// An error that can occur when making a request to
|
||||
/// an JSON-RPC server.
|
||||
#[error("RPC error: {0}")]
|
||||
RpcError(#[from] RpcError),
|
||||
/// The response from the server could not be SCALE decoded.
|
||||
#[error("Response parse failed: {0}")]
|
||||
ResponseParseFailed(#[from] codec::Error),
|
||||
/// Internal channel error - communication channel is either closed, or full.
|
||||
/// It can be solved with reconnect.
|
||||
#[error("Internal communication channel error: {0:?}.")]
|
||||
ChannelError(String),
|
||||
/// Required teyrchain head is not present at the relay chain.
|
||||
#[error("Teyrchain {0:?} head {1} is missing from the relay chain storage.")]
|
||||
MissingRequiredTeyrchainHead(ParaId, u64),
|
||||
/// Failed to find finality proof for the given header.
|
||||
#[error("Failed to find finality proof for header {0}.")]
|
||||
FinalityProofNotFound(u64),
|
||||
/// The client we're connected to is not synced, so we can't rely on its state.
|
||||
#[error("Bizinikiwi client is not synced {0}.")]
|
||||
ClientNotSynced(Health),
|
||||
/// Failed to get system health.
|
||||
#[error("Failed to get system health of {chain} node: {error:?}.")]
|
||||
FailedToGetSystemHealth {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to read best finalized header hash from given chain.
|
||||
#[error("Failed to read best finalized header hash of {chain}: {error:?}.")]
|
||||
FailedToReadBestFinalizedHeaderHash {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to read best finalized header from given chain.
|
||||
#[error("Failed to read best header of {chain}: {error:?}.")]
|
||||
FailedToReadBestHeader {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to read header hash by number from given chain.
|
||||
#[error("Failed to read header hash by number {number} of {chain}: {error:?}.")]
|
||||
FailedToReadHeaderHashByNumber {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Number of the header we've tried to read.
|
||||
number: String,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to read header by hash from given chain.
|
||||
#[error("Failed to read header {hash} of {chain}: {error:?}.")]
|
||||
FailedToReadHeaderByHash {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Hash of the header we've tried to read.
|
||||
hash: String,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to read block by hash from given chain.
|
||||
#[error("Failed to read block {hash} of {chain}: {error:?}.")]
|
||||
FailedToReadBlockByHash {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Hash of the header we've tried to read.
|
||||
hash: String,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to read sotrage value at given chain.
|
||||
#[error("Failed to read storage value {key:?} at {chain}: {error:?}.")]
|
||||
FailedToReadStorageValue {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Hash of the block we've tried to read value from.
|
||||
hash: String,
|
||||
/// Runtime storage key
|
||||
key: StorageKey,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to read runtime version of given chain.
|
||||
#[error("Failed to read runtime version of {chain}: {error:?}.")]
|
||||
FailedToReadRuntimeVersion {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to get pending extrinsics.
|
||||
#[error("Failed to get pending extrinsics of {chain}: {error:?}.")]
|
||||
FailedToGetPendingExtrinsics {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to submit transaction.
|
||||
#[error("Failed to submit {chain} transaction: {error:?}.")]
|
||||
FailedToSubmitTransaction {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Runtime call has failed.
|
||||
#[error("Runtime call {method} with arguments {arguments:?} of chain {chain} at {hash} has failed: {error:?}.")]
|
||||
FailedStateCall {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Hash of the block we've tried to call at.
|
||||
hash: String,
|
||||
/// Runtime API method.
|
||||
method: String,
|
||||
/// Encoded method arguments.
|
||||
arguments: Bytes,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to prove storage keys.
|
||||
#[error("Failed to prove storage keys {storage_keys:?} of {chain} at {hash}: {error:?}.")]
|
||||
FailedToProveStorage {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Hash of the block we've tried to prove keys at.
|
||||
hash: String,
|
||||
/// Storage keys we have tried to prove.
|
||||
storage_keys: Vec<StorageKey>,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to subscribe to GRANDPA justifications stream.
|
||||
#[error("Failed to subscribe to {chain} best headers: {error:?}.")]
|
||||
FailedToSubscribeBestHeaders {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to subscribe to GRANDPA justifications stream.
|
||||
#[error("Failed to subscribe to {chain} finalized headers: {error:?}.")]
|
||||
FailedToSubscribeFinalizedHeaders {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Failed to subscribe to GRANDPA justifications stream.
|
||||
#[error("Failed to subscribe to {chain} justifications: {error:?}.")]
|
||||
FailedToSubscribeJustifications {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Underlying error.
|
||||
error: Box<Error>,
|
||||
},
|
||||
/// Headers of the chain are finalized out of order. Maybe chain has been
|
||||
/// restarted?
|
||||
#[error("Finalized headers of {chain} are unordered: previously finalized {prev_number} vs new {next_number}")]
|
||||
UnorderedFinalizedHeaders {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Previously finalized header number.
|
||||
prev_number: String,
|
||||
/// New finalized header number.
|
||||
next_number: String,
|
||||
},
|
||||
/// The bridge pezpallet is halted and all transactions will be rejected.
|
||||
#[error("Bridge pezpallet is halted.")]
|
||||
BridgePalletIsHalted,
|
||||
/// The bridge pezpallet is not yet initialized and all transactions will be rejected.
|
||||
#[error("Bridge pezpallet is not initialized.")]
|
||||
BridgePalletIsNotInitialized,
|
||||
/// The Bizinikiwi transaction is invalid.
|
||||
#[error("Bizinikiwi transaction is invalid: {0:?}")]
|
||||
TransactionInvalid(#[from] TransactionValidityError),
|
||||
/// The client is configured to use newer runtime version than the connected chain uses.
|
||||
/// The client will keep waiting until chain is upgraded to given version.
|
||||
#[error("Waiting for {chain} runtime upgrade: expected {expected:?} actual {actual:?}")]
|
||||
WaitingForRuntimeUpgrade {
|
||||
/// Name of the chain where the error has happened.
|
||||
chain: String,
|
||||
/// Expected runtime version.
|
||||
expected: SimpleRuntimeVersion,
|
||||
/// Actual runtime version.
|
||||
actual: SimpleRuntimeVersion,
|
||||
},
|
||||
/// Finality proof submission exceeds size and/or weight limits.
|
||||
#[error("Finality proof submission exceeds limits: {extras:?}")]
|
||||
FinalityProofWeightLimitExceeded {
|
||||
/// Finality proof submission extras.
|
||||
extras: SubmitFinalityProofCallExtras,
|
||||
},
|
||||
/// Custom logic error.
|
||||
#[error("{0}")]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl From<tokio::task::JoinError> for Error {
|
||||
fn from(error: tokio::task::JoinError) -> Self {
|
||||
Error::ChannelError(format!("failed to wait tokio task: {error}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<async_std::channel::TrySendError<T>> for Error {
|
||||
fn from(error: async_std::channel::TrySendError<T>) -> Self {
|
||||
Error::ChannelError(format!("`try_send` has failed: {error:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<async_std::channel::RecvError> for Error {
|
||||
fn from(error: async_std::channel::RecvError) -> Self {
|
||||
Error::ChannelError(format!("`recv` has failed: {error:?}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Box the error.
|
||||
pub fn boxed(self) -> Box<Self> {
|
||||
Box::new(self)
|
||||
}
|
||||
|
||||
/// Returns nested error reference.
|
||||
pub fn nested(&self) -> Option<&Self> {
|
||||
match *self {
|
||||
Self::FailedToReadBestFinalizedHeaderHash { ref error, .. } => Some(&**error),
|
||||
Self::FailedToReadBestHeader { ref error, .. } => Some(&**error),
|
||||
Self::FailedToReadHeaderHashByNumber { ref error, .. } => Some(&**error),
|
||||
Self::FailedToReadHeaderByHash { ref error, .. } => Some(&**error),
|
||||
Self::FailedToReadBlockByHash { ref error, .. } => Some(&**error),
|
||||
Self::FailedToReadStorageValue { ref error, .. } => Some(&**error),
|
||||
Self::FailedToReadRuntimeVersion { ref error, .. } => Some(&**error),
|
||||
Self::FailedToGetPendingExtrinsics { ref error, .. } => Some(&**error),
|
||||
Self::FailedToSubmitTransaction { ref error, .. } => Some(&**error),
|
||||
Self::FailedStateCall { ref error, .. } => Some(&**error),
|
||||
Self::FailedToProveStorage { ref error, .. } => Some(&**error),
|
||||
Self::FailedToGetSystemHealth { ref error, .. } => Some(&**error),
|
||||
Self::FailedToSubscribeBestHeaders { ref error, .. } => Some(&**error),
|
||||
Self::FailedToSubscribeFinalizedHeaders { ref error, .. } => Some(&**error),
|
||||
Self::FailedToSubscribeJustifications { ref error, .. } => Some(&**error),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs `FailedToReadHeaderHashByNumber` variant.
|
||||
pub fn failed_to_read_header_hash_by_number<C: Chain>(
|
||||
number: BlockNumberOf<C>,
|
||||
e: Error,
|
||||
) -> Self {
|
||||
Error::FailedToReadHeaderHashByNumber {
|
||||
chain: C::NAME.into(),
|
||||
number: format!("{number}"),
|
||||
error: e.boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs `FailedToReadHeaderByHash` variant.
|
||||
pub fn failed_to_read_header_by_hash<C: Chain>(hash: HashOf<C>, e: Error) -> Self {
|
||||
Error::FailedToReadHeaderByHash {
|
||||
chain: C::NAME.into(),
|
||||
hash: format!("{hash}"),
|
||||
error: e.boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs `FailedToReadBlockByHash` variant.
|
||||
pub fn failed_to_read_block_by_hash<C: Chain>(hash: HashOf<C>, e: Error) -> Self {
|
||||
Error::FailedToReadHeaderByHash {
|
||||
chain: C::NAME.into(),
|
||||
hash: format!("{hash}"),
|
||||
error: e.boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs `FailedToReadBestFinalizedHeaderHash` variant.
|
||||
pub fn failed_to_read_best_finalized_header_hash<C: Chain>(e: Error) -> Self {
|
||||
Error::FailedToReadBestFinalizedHeaderHash { chain: C::NAME.into(), error: e.boxed() }
|
||||
}
|
||||
|
||||
/// Constructs `FailedToReadBestHeader` variant.
|
||||
pub fn failed_to_read_best_header<C: Chain>(e: Error) -> Self {
|
||||
Error::FailedToReadBestHeader { chain: C::NAME.into(), error: e.boxed() }
|
||||
}
|
||||
|
||||
/// Constructs `FailedToReadRuntimeVersion` variant.
|
||||
pub fn failed_to_read_runtime_version<C: Chain>(e: Error) -> Self {
|
||||
Error::FailedToReadRuntimeVersion { chain: C::NAME.into(), error: e.boxed() }
|
||||
}
|
||||
|
||||
/// Constructs `FailedToReadStorageValue` variant.
|
||||
pub fn failed_to_read_storage_value<C: Chain>(
|
||||
at: HashOf<C>,
|
||||
key: StorageKey,
|
||||
e: Error,
|
||||
) -> Self {
|
||||
Error::FailedToReadStorageValue {
|
||||
chain: C::NAME.into(),
|
||||
hash: format!("{at}"),
|
||||
key,
|
||||
error: e.boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs `FailedToGetPendingExtrinsics` variant.
|
||||
pub fn failed_to_get_pending_extrinsics<C: Chain>(e: Error) -> Self {
|
||||
Error::FailedToGetPendingExtrinsics { chain: C::NAME.into(), error: e.boxed() }
|
||||
}
|
||||
|
||||
/// Constructs `FailedToSubmitTransaction` variant.
|
||||
pub fn failed_to_submit_transaction<C: Chain>(e: Error) -> Self {
|
||||
Error::FailedToSubmitTransaction { chain: C::NAME.into(), error: e.boxed() }
|
||||
}
|
||||
|
||||
/// Constructs `FailedStateCall` variant.
|
||||
pub fn failed_state_call<C: Chain>(
|
||||
at: HashOf<C>,
|
||||
method: String,
|
||||
arguments: Bytes,
|
||||
e: Error,
|
||||
) -> Self {
|
||||
Error::FailedStateCall {
|
||||
chain: C::NAME.into(),
|
||||
hash: format!("{at}"),
|
||||
method,
|
||||
arguments,
|
||||
error: e.boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs `FailedToProveStorage` variant.
|
||||
pub fn failed_to_prove_storage<C: Chain>(
|
||||
at: HashOf<C>,
|
||||
storage_keys: Vec<StorageKey>,
|
||||
e: Error,
|
||||
) -> Self {
|
||||
Error::FailedToProveStorage {
|
||||
chain: C::NAME.into(),
|
||||
hash: format!("{at}"),
|
||||
storage_keys,
|
||||
error: e.boxed(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs `FailedToGetSystemHealth` variant.
|
||||
pub fn failed_to_get_system_health<C: Chain>(e: Error) -> Self {
|
||||
Error::FailedToGetSystemHealth { chain: C::NAME.into(), error: e.boxed() }
|
||||
}
|
||||
|
||||
/// Constructs `FailedToSubscribeBestHeaders` variant.
|
||||
pub fn failed_to_subscribe_best_headers<C: Chain>(e: Error) -> Self {
|
||||
Error::FailedToSubscribeBestHeaders { chain: C::NAME.into(), error: e.boxed() }
|
||||
}
|
||||
|
||||
/// Constructs `FailedToSubscribeFinalizedHeaders` variant.
|
||||
pub fn failed_to_subscribe_finalized_headers<C: Chain>(e: Error) -> Self {
|
||||
Error::FailedToSubscribeFinalizedHeaders { chain: C::NAME.into(), error: e.boxed() }
|
||||
}
|
||||
|
||||
/// Constructs `FailedToSubscribeJustifications` variant.
|
||||
pub fn failed_to_subscribe_justification<C: Chain>(e: Error) -> Self {
|
||||
Error::FailedToSubscribeJustifications { chain: C::NAME.into(), error: e.boxed() }
|
||||
}
|
||||
|
||||
/// Constructs `Un`
|
||||
pub fn unordered_finalized_headers<C: Chain>(
|
||||
prev_number: BlockNumberOf<C>,
|
||||
next_number: BlockNumberOf<C>,
|
||||
) -> Self {
|
||||
Error::UnorderedFinalizedHeaders {
|
||||
chain: C::NAME.into(),
|
||||
prev_number: format!("{}", prev_number),
|
||||
next_number: format!("{}", next_number),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeConnectionError for Error {
|
||||
fn is_connection_error(&self) -> bool {
|
||||
match *self {
|
||||
Error::ChannelError(_) => true,
|
||||
Error::RpcError(ref e) => {
|
||||
matches!(*e, RpcError::Transport(_) | RpcError::RestartNeeded(_),)
|
||||
},
|
||||
Error::ClientNotSynced(_) => true,
|
||||
Error::UnorderedFinalizedHeaders { .. } => true,
|
||||
_ => self.nested().map(|e| e.is_connection_error()).unwrap_or(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Pezpallet provides a set of guard functions that are running in background threads
|
||||
//! and are aborting process if some condition fails.
|
||||
|
||||
use crate::{error::Error, Chain, Client};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use pezsp_version::RuntimeVersion;
|
||||
use std::{
|
||||
fmt::Display,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// Guards environment.
|
||||
#[async_trait]
|
||||
pub trait Environment<C>: Send + Sync + 'static {
|
||||
/// Error type.
|
||||
type Error: Display + Send + Sync + 'static;
|
||||
|
||||
/// Return current runtime version.
|
||||
async fn runtime_version(&mut self) -> Result<RuntimeVersion, Self::Error>;
|
||||
|
||||
/// Return current time.
|
||||
fn now(&self) -> Instant {
|
||||
Instant::now()
|
||||
}
|
||||
|
||||
/// Sleep given amount of time.
|
||||
async fn sleep(&mut self, duration: Duration) {
|
||||
async_std::task::sleep(duration).await
|
||||
}
|
||||
|
||||
/// Abort current process. Called when guard condition check fails.
|
||||
async fn abort(&mut self) {
|
||||
std::process::abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// Abort when runtime spec version is different from specified.
|
||||
pub fn abort_on_spec_version_change<C: Chain>(
|
||||
mut env: impl Environment<C>,
|
||||
expected_spec_version: u32,
|
||||
) {
|
||||
async_std::task::spawn(async move {
|
||||
tracing::info!(
|
||||
target: "bridge-guard",
|
||||
node=%C::NAME,
|
||||
%expected_spec_version,
|
||||
"Starting spec_version guard."
|
||||
);
|
||||
|
||||
loop {
|
||||
let actual_spec_version = env.runtime_version().await;
|
||||
match actual_spec_version {
|
||||
Ok(version) if version.spec_version == expected_spec_version => (),
|
||||
Ok(version) => {
|
||||
tracing::error!(
|
||||
target: "bridge-guard",
|
||||
node=%C::NAME,
|
||||
from=%expected_spec_version,
|
||||
to=%version.spec_version,
|
||||
"Runtime spec version has changed. Aborting relay"
|
||||
);
|
||||
|
||||
env.abort().await;
|
||||
},
|
||||
Err(error) => tracing::warn!(
|
||||
target: "bridge-guard",
|
||||
%error,
|
||||
node=%C::NAME,
|
||||
"Failed to read runtime version. Relay may need to be stopped manually"
|
||||
),
|
||||
}
|
||||
|
||||
env.sleep(conditions_check_delay::<C>()).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Delay between conditions check.
|
||||
fn conditions_check_delay<C: Chain>() -> Duration {
|
||||
C::AVERAGE_BLOCK_INTERVAL * (10 + rand::random::<u32>() % 10)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: Chain, Clnt: Client<C>> Environment<C> for Clnt {
|
||||
type Error = Error;
|
||||
|
||||
async fn runtime_version(&mut self) -> Result<RuntimeVersion, Self::Error> {
|
||||
Client::<C>::runtime_version(self).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
use crate::test_chain::TestChain;
|
||||
use futures::{
|
||||
channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
|
||||
future::FutureExt,
|
||||
stream::StreamExt,
|
||||
SinkExt,
|
||||
};
|
||||
|
||||
pub struct TestEnvironment {
|
||||
pub runtime_version_rx: UnboundedReceiver<RuntimeVersion>,
|
||||
pub slept_tx: UnboundedSender<()>,
|
||||
pub aborted_tx: UnboundedSender<()>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Environment<TestChain> for TestEnvironment {
|
||||
type Error = Error;
|
||||
|
||||
async fn runtime_version(&mut self) -> Result<RuntimeVersion, Self::Error> {
|
||||
Ok(self.runtime_version_rx.next().await.unwrap_or_default())
|
||||
}
|
||||
|
||||
async fn sleep(&mut self, _duration: Duration) {
|
||||
let _ = self.slept_tx.send(()).await;
|
||||
}
|
||||
|
||||
async fn abort(&mut self) {
|
||||
let _ = self.aborted_tx.send(()).await;
|
||||
// simulate process abort :)
|
||||
async_std::task::sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aborts_when_spec_version_is_changed() {
|
||||
async_std::task::block_on(async {
|
||||
let (
|
||||
(mut runtime_version_tx, runtime_version_rx),
|
||||
(slept_tx, mut slept_rx),
|
||||
(aborted_tx, mut aborted_rx),
|
||||
) = (unbounded(), unbounded(), unbounded());
|
||||
abort_on_spec_version_change(
|
||||
TestEnvironment { runtime_version_rx, slept_tx, aborted_tx },
|
||||
0,
|
||||
);
|
||||
|
||||
// client responds with wrong version
|
||||
runtime_version_tx
|
||||
.send(RuntimeVersion { spec_version: 42, ..Default::default() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// then the `abort` function is called
|
||||
aborted_rx.next().await;
|
||||
// and we do not reach the `sleep` function call
|
||||
assert!(slept_rx.next().now_or_never().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_aborts_when_spec_version_is_unchanged() {
|
||||
async_std::task::block_on(async {
|
||||
let (
|
||||
(mut runtime_version_tx, runtime_version_rx),
|
||||
(slept_tx, mut slept_rx),
|
||||
(aborted_tx, mut aborted_rx),
|
||||
) = (unbounded(), unbounded(), unbounded());
|
||||
abort_on_spec_version_change(
|
||||
TestEnvironment { runtime_version_rx, slept_tx, aborted_tx },
|
||||
42,
|
||||
);
|
||||
|
||||
// client responds with the same version
|
||||
runtime_version_tx
|
||||
.send(RuntimeVersion { spec_version: 42, ..Default::default() })
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// then the `sleep` function is called
|
||||
slept_rx.next().await;
|
||||
// and the `abort` function is not called
|
||||
assert!(aborted_rx.next().now_or_never().is_none());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Tools to interact with Bizinikiwi node using RPC methods.
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod chain;
|
||||
mod client;
|
||||
mod error;
|
||||
mod sync_header;
|
||||
mod transaction_tracker;
|
||||
|
||||
pub mod calls;
|
||||
pub mod guard;
|
||||
pub mod metrics;
|
||||
pub mod test_chain;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
pub use crate::{
|
||||
chain::{
|
||||
AccountKeyPairOf, BlockWithJustification, CallOf, Chain, ChainWithBalances,
|
||||
ChainWithGrandpa, ChainWithMessages, ChainWithRewards, ChainWithRuntimeVersion,
|
||||
ChainWithTransactions, ChainWithUtilityPallet, FullRuntimeUtilityPallet,
|
||||
MockedRuntimeUtilityPallet, RelayChain, SignParam, SignedBlockOf, Teyrchain,
|
||||
TransactionStatusOf, UnsignedTransaction, UtilityPallet,
|
||||
},
|
||||
client::{
|
||||
is_ancient_block, rpc_with_caching as new, ChainRuntimeVersion, Client,
|
||||
OpaqueGrandpaAuthoritiesSet, RpcWithCachingClient, SimpleRuntimeVersion, StreamDescription,
|
||||
Subscription, ANCIENT_BLOCK_THRESHOLD,
|
||||
},
|
||||
error::{Error, Result},
|
||||
sync_header::SyncHeader,
|
||||
transaction_tracker::TransactionTracker,
|
||||
};
|
||||
pub use pezbp_runtime::{
|
||||
AccountIdOf, AccountPublicOf, BalanceOf, BlockNumberOf, Chain as ChainBase, HashOf, HeaderIdOf,
|
||||
HeaderOf, NonceOf, SignatureOf, Teyrchain as TeyrchainBase, TransactionEra, TransactionEraOf,
|
||||
UnderlyingChainProvider,
|
||||
};
|
||||
|
||||
/// Bizinikiwi-over-websocket connection params.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConnectionParams {
|
||||
/// Websocket endpoint URL.
|
||||
pub uri: String,
|
||||
/// Defined chain runtime version
|
||||
pub chain_runtime_version: ChainRuntimeVersion,
|
||||
}
|
||||
|
||||
impl Default for ConnectionParams {
|
||||
fn default() -> Self {
|
||||
ConnectionParams {
|
||||
uri: "ws://localhost:9944".into(),
|
||||
chain_runtime_version: ChainRuntimeVersion::Auto,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns stall timeout for relay loop.
|
||||
///
|
||||
/// Relay considers himself stalled if he has submitted transaction to the node, but it has not
|
||||
/// been mined for this period.
|
||||
pub fn transaction_stall_timeout(
|
||||
mortality_period: Option<u32>,
|
||||
average_block_interval: Duration,
|
||||
default_stall_timeout: Duration,
|
||||
) -> Duration {
|
||||
// 1 extra block for transaction to reach the pool && 1 for relayer to awake after it is mined
|
||||
mortality_period
|
||||
.map(|mortality_period| average_block_interval.saturating_mul(mortality_period + 1 + 1))
|
||||
.unwrap_or(default_stall_timeout)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{Chain, Client, Error as BizinikiwiError};
|
||||
|
||||
use async_std::sync::{Arc, RwLock};
|
||||
use async_trait::async_trait;
|
||||
use codec::Decode;
|
||||
use num_traits::One;
|
||||
use relay_utils::metrics::{
|
||||
metric_name, register, F64SharedRef, Gauge, Metric, PrometheusError, Registry,
|
||||
StandaloneMetric, F64,
|
||||
};
|
||||
use pezsp_core::storage::{StorageData, StorageKey};
|
||||
use pezsp_runtime::{traits::UniqueSaturatedInto, FixedPointNumber, FixedU128};
|
||||
use std::{marker::PhantomData, time::Duration};
|
||||
|
||||
/// Storage value update interval (in blocks).
|
||||
const UPDATE_INTERVAL_IN_BLOCKS: u32 = 5;
|
||||
|
||||
/// Fied-point storage value and the way it is decoded from the raw storage value.
|
||||
pub trait FloatStorageValue: 'static + Clone + Send + Sync {
|
||||
/// Type of the value.
|
||||
type Value: FixedPointNumber;
|
||||
/// Try to decode value from the raw storage value.
|
||||
fn decode(
|
||||
&self,
|
||||
maybe_raw_value: Option<StorageData>,
|
||||
) -> Result<Option<Self::Value>, BizinikiwiError>;
|
||||
}
|
||||
|
||||
/// Implementation of `FloatStorageValue` that expects encoded `FixedU128` value and returns `1` if
|
||||
/// value is missing from the storage.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct FixedU128OrOne;
|
||||
|
||||
impl FloatStorageValue for FixedU128OrOne {
|
||||
type Value = FixedU128;
|
||||
|
||||
fn decode(
|
||||
&self,
|
||||
maybe_raw_value: Option<StorageData>,
|
||||
) -> Result<Option<Self::Value>, BizinikiwiError> {
|
||||
maybe_raw_value
|
||||
.map(|raw_value| {
|
||||
FixedU128::decode(&mut &raw_value.0[..])
|
||||
.map_err(BizinikiwiError::ResponseParseFailed)
|
||||
.map(Some)
|
||||
})
|
||||
.unwrap_or_else(|| Ok(Some(FixedU128::one())))
|
||||
}
|
||||
}
|
||||
|
||||
/// Metric that represents fixed-point runtime storage value as float gauge.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FloatStorageValueMetric<C, Clnt, V> {
|
||||
value_converter: V,
|
||||
client: Clnt,
|
||||
storage_key: StorageKey,
|
||||
metric: Gauge<F64>,
|
||||
shared_value_ref: F64SharedRef,
|
||||
_phantom: PhantomData<(C, V)>,
|
||||
}
|
||||
|
||||
impl<C, Clnt, V> FloatStorageValueMetric<C, Clnt, V> {
|
||||
/// Create new metric.
|
||||
pub fn new(
|
||||
value_converter: V,
|
||||
client: Clnt,
|
||||
storage_key: StorageKey,
|
||||
name: String,
|
||||
help: String,
|
||||
) -> Result<Self, PrometheusError> {
|
||||
let shared_value_ref = Arc::new(RwLock::new(None));
|
||||
Ok(FloatStorageValueMetric {
|
||||
value_converter,
|
||||
client,
|
||||
storage_key,
|
||||
metric: Gauge::new(metric_name(None, &name), help)?,
|
||||
shared_value_ref,
|
||||
_phantom: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get shared reference to metric value.
|
||||
pub fn shared_value_ref(&self) -> F64SharedRef {
|
||||
self.shared_value_ref.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Chain, Clnt: Client<C>, V: FloatStorageValue> Metric
|
||||
for FloatStorageValueMetric<C, Clnt, V>
|
||||
{
|
||||
fn register(&self, registry: &Registry) -> Result<(), PrometheusError> {
|
||||
register(self.metric.clone(), registry).map(drop)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: Chain, Clnt: Client<C>, V: FloatStorageValue> StandaloneMetric
|
||||
for FloatStorageValueMetric<C, Clnt, V>
|
||||
{
|
||||
fn update_interval(&self) -> Duration {
|
||||
C::AVERAGE_BLOCK_INTERVAL * UPDATE_INTERVAL_IN_BLOCKS
|
||||
}
|
||||
|
||||
async fn update(&self) {
|
||||
let value = async move {
|
||||
let best_header_hash = self.client.best_header_hash().await?;
|
||||
let maybe_storage_value = self
|
||||
.client
|
||||
.raw_storage_value(best_header_hash, self.storage_key.clone())
|
||||
.await?;
|
||||
self.value_converter.decode(maybe_storage_value).map(|maybe_fixed_point_value| {
|
||||
maybe_fixed_point_value.map(|fixed_point_value| {
|
||||
fixed_point_value.into_inner().unique_saturated_into() as f64 /
|
||||
V::Value::DIV.unique_saturated_into() as f64
|
||||
})
|
||||
})
|
||||
}
|
||||
.await
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
relay_utils::metrics::set_gauge_value(&self.metric, value.clone());
|
||||
*self.shared_value_ref.write().await = value.ok().and_then(|x| x);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Contains several Bizinikiwi-specific metrics that may be exposed by relay.
|
||||
|
||||
pub use float_storage_value::{FixedU128OrOne, FloatStorageValue, FloatStorageValueMetric};
|
||||
|
||||
mod float_storage_value;
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
use bp_header_pez_chain::ConsensusLogReader;
|
||||
use pez_finality_relay::SourceHeader as FinalitySourceHeader;
|
||||
use pezsp_runtime::traits::Header as HeaderT;
|
||||
|
||||
/// Generic wrapper for `pezsp_runtime::traits::Header` based headers, that
|
||||
/// implements `pez_finality_relay::SourceHeader` and may be used in headers sync directly.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SyncHeader<Header>(Header);
|
||||
|
||||
impl<Header> SyncHeader<Header> {
|
||||
/// Extracts wrapped header from self.
|
||||
pub fn into_inner(self) -> Header {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<Header> std::ops::Deref for SyncHeader<Header> {
|
||||
type Target = Header;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<Header> From<Header> for SyncHeader<Header> {
|
||||
fn from(header: Header) -> Self {
|
||||
Self(header)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Header: HeaderT, R: ConsensusLogReader> FinalitySourceHeader<Header::Hash, Header::Number, R>
|
||||
for SyncHeader<Header>
|
||||
{
|
||||
fn hash(&self) -> Header::Hash {
|
||||
self.0.hash()
|
||||
}
|
||||
|
||||
fn number(&self) -> Header::Number {
|
||||
*self.0.number()
|
||||
}
|
||||
|
||||
fn is_mandatory(&self) -> bool {
|
||||
R::schedules_authorities_change(self.digest())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Pezpallet provides a set of guard functions that are running in background threads
|
||||
//! and are aborting process if some condition fails.
|
||||
|
||||
//! Test chain implementation to use in tests.
|
||||
|
||||
#![cfg(any(feature = "test-helpers", test))]
|
||||
|
||||
use crate::{
|
||||
Chain, ChainWithBalances, ChainWithMessages, ChainWithRewards, ChainWithTransactions,
|
||||
Error as BizinikiwiError, SignParam, UnsignedTransaction,
|
||||
};
|
||||
use bp_messages::{ChainWithMessages as ChainWithMessagesBase, MessageNonce};
|
||||
use pezbp_runtime::ChainId;
|
||||
use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
|
||||
use pezframe_support::{pezsp_runtime::StateVersion, weights::Weight};
|
||||
use scale_info::TypeInfo;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Chain that may be used in tests.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TestChain;
|
||||
|
||||
impl pezbp_runtime::Chain for TestChain {
|
||||
const ID: ChainId = *b"test";
|
||||
|
||||
type BlockNumber = u32;
|
||||
type Hash = pezsp_core::H256;
|
||||
type Hasher = pezsp_runtime::traits::BlakeTwo256;
|
||||
type Header = pezsp_runtime::generic::Header<u32, pezsp_runtime::traits::BlakeTwo256>;
|
||||
|
||||
type AccountId = u32;
|
||||
type Balance = u32;
|
||||
type Nonce = u32;
|
||||
type Signature = pezsp_runtime::testing::TestSignature;
|
||||
|
||||
const STATE_VERSION: StateVersion = StateVersion::V1;
|
||||
|
||||
fn max_extrinsic_size() -> u32 {
|
||||
100000
|
||||
}
|
||||
|
||||
fn max_extrinsic_weight() -> Weight {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl Chain for TestChain {
|
||||
const NAME: &'static str = "Test";
|
||||
const BEST_FINALIZED_HEADER_ID_METHOD: &'static str = "TestMethod";
|
||||
const FREE_HEADERS_INTERVAL_METHOD: &'static str = "TestMethod";
|
||||
const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_millis(0);
|
||||
|
||||
type SignedBlock = pezsp_runtime::generic::SignedBlock<
|
||||
pezsp_runtime::generic::Block<Self::Header, pezsp_runtime::OpaqueExtrinsic>,
|
||||
>;
|
||||
type Call = TestRuntimeCall;
|
||||
}
|
||||
|
||||
impl ChainWithBalances for TestChain {
|
||||
fn account_info_storage_key(_account_id: &u32) -> pezsp_core::storage::StorageKey {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Reward type for the test chain.
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Decode,
|
||||
DecodeWithMemTracking,
|
||||
Encode,
|
||||
Eq,
|
||||
MaxEncodedLen,
|
||||
PartialEq,
|
||||
TypeInfo,
|
||||
)]
|
||||
pub enum ChainReward {
|
||||
/// Reward 1 type.
|
||||
Reward1,
|
||||
}
|
||||
|
||||
impl ChainWithRewards for TestChain {
|
||||
const WITH_CHAIN_RELAYERS_PALLET_NAME: Option<&'static str> = None;
|
||||
type RewardBalance = u128;
|
||||
type Reward = ChainReward;
|
||||
|
||||
fn account_reward_storage_key(
|
||||
_account_id: &Self::AccountId,
|
||||
_reward: impl Into<Self::Reward>,
|
||||
) -> pezsp_core::storage::StorageKey {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainWithMessagesBase for TestChain {
|
||||
const WITH_CHAIN_MESSAGES_PALLET_NAME: &'static str = "Test";
|
||||
const MAX_UNREWARDED_RELAYERS_IN_CONFIRMATION_TX: MessageNonce = 0;
|
||||
const MAX_UNCONFIRMED_MESSAGES_IN_CONFIRMATION_TX: MessageNonce = 0;
|
||||
}
|
||||
|
||||
impl ChainWithMessages for TestChain {
|
||||
const TO_CHAIN_MESSAGE_DETAILS_METHOD: &'static str = "TestMessagesDetailsMethod";
|
||||
const FROM_CHAIN_MESSAGE_DETAILS_METHOD: &'static str = "TestFromMessagesDetailsMethod";
|
||||
}
|
||||
|
||||
impl ChainWithTransactions for TestChain {
|
||||
type AccountKeyPair = pezsp_core::sr25519::Pair;
|
||||
type SignedTransaction = bp_pezkuwi_core::UncheckedExtrinsic<
|
||||
TestRuntimeCall,
|
||||
bp_pezkuwi_core::SuffixedCommonTransactionExtension<(
|
||||
pezbp_runtime::extensions::BridgeRejectObsoleteHeadersAndMessages,
|
||||
pezbp_runtime::extensions::RefundBridgedTeyrchainMessagesSchema,
|
||||
)>,
|
||||
>;
|
||||
|
||||
fn sign_transaction(
|
||||
_param: SignParam<Self>,
|
||||
_unsigned: UnsignedTransaction<Self>,
|
||||
) -> Result<Self::SignedTransaction, BizinikiwiError> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
/// Dummy runtime call.
|
||||
#[derive(Decode, Encode, Clone, Debug, PartialEq)]
|
||||
pub enum TestRuntimeCall {
|
||||
/// Dummy call.
|
||||
#[codec(index = 0)]
|
||||
Dummy,
|
||||
}
|
||||
|
||||
/// Primitives-level teyrchain that may be used in tests.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TestTeyrchainBase;
|
||||
|
||||
impl pezbp_runtime::Chain for TestTeyrchainBase {
|
||||
const ID: ChainId = *b"tstp";
|
||||
|
||||
type BlockNumber = u32;
|
||||
type Hash = pezsp_core::H256;
|
||||
type Hasher = pezsp_runtime::traits::BlakeTwo256;
|
||||
type Header = pezsp_runtime::generic::Header<u32, pezsp_runtime::traits::BlakeTwo256>;
|
||||
|
||||
type AccountId = u32;
|
||||
type Balance = u32;
|
||||
type Nonce = u32;
|
||||
type Signature = pezsp_runtime::testing::TestSignature;
|
||||
|
||||
const STATE_VERSION: StateVersion = StateVersion::V1;
|
||||
|
||||
fn max_extrinsic_size() -> u32 {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn max_extrinsic_weight() -> Weight {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
impl pezbp_runtime::Teyrchain for TestTeyrchainBase {
|
||||
const TEYRCHAIN_ID: u32 = 1000;
|
||||
const MAX_HEADER_SIZE: u32 = 1_024;
|
||||
}
|
||||
|
||||
/// Teyrchain that may be used in tests.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TestTeyrchain;
|
||||
|
||||
impl pezbp_runtime::UnderlyingChainProvider for TestTeyrchain {
|
||||
type Chain = TestTeyrchainBase;
|
||||
}
|
||||
|
||||
impl Chain for TestTeyrchain {
|
||||
const NAME: &'static str = "TestTeyrchain";
|
||||
const BEST_FINALIZED_HEADER_ID_METHOD: &'static str = "TestTeyrchainMethod";
|
||||
const FREE_HEADERS_INTERVAL_METHOD: &'static str = "TestTeyrchainMethod";
|
||||
const AVERAGE_BLOCK_INTERVAL: Duration = Duration::from_millis(0);
|
||||
|
||||
type SignedBlock = pezsp_runtime::generic::SignedBlock<
|
||||
pezsp_runtime::generic::Block<Self::Header, pezsp_runtime::OpaqueExtrinsic>,
|
||||
>;
|
||||
type Call = TestRuntimeCall;
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
// Copyright 2019-2021 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Helper for tracking transaction invalidation events.
|
||||
|
||||
use crate::{Chain, Error, HashOf, HeaderIdOf, Subscription, TransactionStatusOf};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use futures::{future::Either, Future, FutureExt, Stream, StreamExt};
|
||||
use relay_utils::{HeaderId, TrackedTransactionStatus};
|
||||
use pezsp_runtime::traits::Header as _;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Transaction tracker environment.
|
||||
#[async_trait]
|
||||
pub trait Environment<C: Chain>: Send + Sync {
|
||||
/// Returns header id by its hash.
|
||||
async fn header_id_by_hash(&self, hash: HashOf<C>) -> Result<HeaderIdOf<C>, Error>;
|
||||
}
|
||||
|
||||
// TODO (https://github.com/pezkuwichain/kurdistan-sdk/issues/84): remove `Environment` trait
|
||||
// after test client is implemented
|
||||
#[async_trait]
|
||||
impl<C: Chain, T: crate::client::Client<C>> Environment<C> for T {
|
||||
async fn header_id_by_hash(&self, hash: HashOf<C>) -> Result<HeaderIdOf<C>, Error> {
|
||||
self.header_by_hash(hash).await.map(|h| HeaderId(*h.number(), hash))
|
||||
}
|
||||
}
|
||||
|
||||
/// Bizinikiwi transaction tracker implementation.
|
||||
///
|
||||
/// Bizinikiwi node provides RPC API to submit and watch for transaction events. This way
|
||||
/// we may know when transaction is included into block, finalized or rejected. There are
|
||||
/// some edge cases, when we can't fully trust this mechanism - e.g. transaction may broadcasted
|
||||
/// and then dropped out of node transaction pool (some other cases are also possible - node
|
||||
/// restarts, connection lost, ...). Then we can't know for sure - what is currently happening
|
||||
/// with our transaction. Is the transaction really lost? Is it still alive on the chain network?
|
||||
///
|
||||
/// We have several options to handle such cases:
|
||||
///
|
||||
/// 1) hope that the transaction is still alive and wait for its mining until it is spoiled;
|
||||
///
|
||||
/// 2) assume that the transaction is lost and resubmit another transaction instantly;
|
||||
///
|
||||
/// 3) wait for some time (if transaction is mortal - then until block where it dies; if it is
|
||||
/// immortal - then for some time that we assume is long enough to mine it) and assume that it is
|
||||
/// lost.
|
||||
///
|
||||
/// This struct implements third option as it seems to be the most optimal.
|
||||
pub struct TransactionTracker<C: Chain, E> {
|
||||
environment: E,
|
||||
transaction_hash: HashOf<C>,
|
||||
stall_timeout: Duration,
|
||||
subscription: Subscription<TransactionStatusOf<C>>,
|
||||
}
|
||||
|
||||
impl<C: Chain, E: Environment<C>> TransactionTracker<C, E> {
|
||||
/// Create transaction tracker.
|
||||
pub fn new(
|
||||
environment: E,
|
||||
stall_timeout: Duration,
|
||||
transaction_hash: HashOf<C>,
|
||||
subscription: Subscription<TransactionStatusOf<C>>,
|
||||
) -> Self {
|
||||
Self { environment, stall_timeout, transaction_hash, subscription }
|
||||
}
|
||||
|
||||
// TODO (https://github.com/pezkuwichain/kurdistan-sdk/issues/84): remove me after
|
||||
// test client is implemented
|
||||
/// Converts self into tracker with different environment.
|
||||
pub fn switch_environment<NewE: Environment<C>>(
|
||||
self,
|
||||
environment: NewE,
|
||||
) -> TransactionTracker<C, NewE> {
|
||||
TransactionTracker {
|
||||
environment,
|
||||
stall_timeout: self.stall_timeout,
|
||||
transaction_hash: self.transaction_hash,
|
||||
subscription: self.subscription,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for final transaction status and return it along with last known internal invalidation
|
||||
/// status.
|
||||
async fn do_wait(
|
||||
self,
|
||||
wait_for_stall_timeout: impl Future<Output = ()>,
|
||||
wait_for_stall_timeout_rest: impl Future<Output = ()>,
|
||||
) -> (TrackedTransactionStatus<HeaderIdOf<C>>, Option<InvalidationStatus<HeaderIdOf<C>>>) {
|
||||
// sometimes we want to wait for the rest of the stall timeout even if
|
||||
// `wait_for_invalidation` has been "select"ed first => it is shared
|
||||
let wait_for_invalidation = watch_transaction_status::<_, C, _>(
|
||||
self.environment,
|
||||
self.transaction_hash,
|
||||
self.subscription,
|
||||
);
|
||||
futures::pin_mut!(wait_for_stall_timeout, wait_for_invalidation);
|
||||
|
||||
match futures::future::select(wait_for_stall_timeout, wait_for_invalidation).await {
|
||||
Either::Left((_, _)) => {
|
||||
tracing::trace!(
|
||||
target: "bridge",
|
||||
node=%C::NAME,
|
||||
transaction=?self.transaction_hash,
|
||||
"Transaction is considered lost after timeout (no status response from the node)"
|
||||
);
|
||||
|
||||
(TrackedTransactionStatus::Lost, None)
|
||||
},
|
||||
Either::Right((invalidation_status, _)) => match invalidation_status {
|
||||
InvalidationStatus::Finalized(at_block) =>
|
||||
(TrackedTransactionStatus::Finalized(at_block), Some(invalidation_status)),
|
||||
InvalidationStatus::Invalid =>
|
||||
(TrackedTransactionStatus::Lost, Some(invalidation_status)),
|
||||
InvalidationStatus::Lost => {
|
||||
// wait for the rest of stall timeout - this way we'll be sure that the
|
||||
// transaction is actually dead if it has been crafted properly
|
||||
wait_for_stall_timeout_rest.await;
|
||||
// if someone is still watching for our transaction, then we're reporting
|
||||
// an error here (which is treated as "transaction lost")
|
||||
tracing::trace!(
|
||||
target: "bridge",
|
||||
node=%C::NAME,
|
||||
transaction=?self.transaction_hash,
|
||||
"Transaction is considered lost after timeout"
|
||||
);
|
||||
|
||||
(TrackedTransactionStatus::Lost, Some(invalidation_status))
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<C: Chain, E: Environment<C>> relay_utils::TransactionTracker for TransactionTracker<C, E> {
|
||||
type HeaderId = HeaderIdOf<C>;
|
||||
|
||||
async fn wait(self) -> TrackedTransactionStatus<HeaderIdOf<C>> {
|
||||
let wait_for_stall_timeout = async_std::task::sleep(self.stall_timeout).shared();
|
||||
let wait_for_stall_timeout_rest = wait_for_stall_timeout.clone();
|
||||
self.do_wait(wait_for_stall_timeout, wait_for_stall_timeout_rest).await.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Transaction invalidation status.
|
||||
///
|
||||
/// Note that in places where the `TransactionTracker` is used, the finalization event will be
|
||||
/// ignored - relay loops are detecting the mining/finalization using their own
|
||||
/// techniques. That's why we're using `InvalidationStatus` here.
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum InvalidationStatus<BlockId> {
|
||||
/// Transaction has been included into block and finalized at given block.
|
||||
Finalized(BlockId),
|
||||
/// Transaction has been invalidated.
|
||||
Invalid,
|
||||
/// We have lost track of transaction status.
|
||||
Lost,
|
||||
}
|
||||
|
||||
/// Watch for transaction status until transaction is finalized or we lose track of its status.
|
||||
async fn watch_transaction_status<
|
||||
E: Environment<C>,
|
||||
C: Chain,
|
||||
S: Stream<Item = TransactionStatusOf<C>>,
|
||||
>(
|
||||
environment: E,
|
||||
transaction_hash: HashOf<C>,
|
||||
subscription: S,
|
||||
) -> InvalidationStatus<HeaderIdOf<C>> {
|
||||
futures::pin_mut!(subscription);
|
||||
|
||||
loop {
|
||||
match subscription.next().await {
|
||||
Some(TransactionStatusOf::<C>::Finalized((block_hash, _))) => {
|
||||
// the only "successful" outcome of this method is when the block with transaction
|
||||
// has been finalized
|
||||
tracing::trace!(
|
||||
target: "bridge",
|
||||
node=%C::NAME,
|
||||
transaction=?transaction_hash,
|
||||
block=?block_hash,
|
||||
"Transaction has been finalized"
|
||||
);
|
||||
|
||||
let header_id = match environment.header_id_by_hash(block_hash).await {
|
||||
Ok(header_id) => header_id,
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
target: "bridge",
|
||||
error=?e,
|
||||
node=%C::NAME,
|
||||
transaction=?transaction_hash,
|
||||
block=?block_hash,
|
||||
"Failed to read header when watching for transaction",
|
||||
);
|
||||
// that's the best option we have here
|
||||
return InvalidationStatus::Lost;
|
||||
},
|
||||
};
|
||||
return InvalidationStatus::Finalized(header_id);
|
||||
},
|
||||
Some(TransactionStatusOf::<C>::Invalid) => {
|
||||
// if node says that the transaction is invalid, there are still chances that
|
||||
// it is not actually invalid - e.g. if the block where transaction has been
|
||||
// revalidated is retracted and transaction (at some other node pool) becomes
|
||||
// valid again on other fork. But let's assume that the chances of this event
|
||||
// are almost zero - there's a lot of things that must happen for this to be the
|
||||
// case.
|
||||
tracing::trace!(
|
||||
target: "bridge",
|
||||
node=%C::NAME,
|
||||
transaction=?transaction_hash,
|
||||
"Transaction has been invalidated"
|
||||
);
|
||||
return InvalidationStatus::Invalid;
|
||||
},
|
||||
Some(TransactionStatusOf::<C>::Future) |
|
||||
Some(TransactionStatusOf::<C>::Ready) |
|
||||
Some(TransactionStatusOf::<C>::Broadcast(_)) => {
|
||||
// nothing important (for us) has happened
|
||||
},
|
||||
Some(TransactionStatusOf::<C>::InBlock(block_hash)) => {
|
||||
// TODO: read matching system event (ExtrinsicSuccess or ExtrinsicFailed), log it
|
||||
// here and use it later (on finality) for reporting invalid transaction
|
||||
// https://github.com/pezkuwichain/kurdistan-sdk/issues/79
|
||||
tracing::trace!(
|
||||
target: "bridge",
|
||||
node=%C::NAME,
|
||||
transaction=?transaction_hash,
|
||||
block=?block_hash,
|
||||
"Transaction has been included"
|
||||
);
|
||||
},
|
||||
Some(TransactionStatusOf::<C>::Retracted(block_hash)) => {
|
||||
tracing::trace!(
|
||||
target: "bridge",
|
||||
node=%C::NAME,
|
||||
transaction=?transaction_hash,
|
||||
block=?block_hash,
|
||||
"Transaction has been retracted"
|
||||
);
|
||||
},
|
||||
Some(TransactionStatusOf::<C>::FinalityTimeout(block_hash)) => {
|
||||
// finality is lagging? let's wait a bit more and report a stall
|
||||
tracing::trace!(
|
||||
target: "bridge",
|
||||
node=%C::NAME,
|
||||
transaction=?transaction_hash,
|
||||
block=?block_hash,
|
||||
"Transaction has not been finalized for too long"
|
||||
);
|
||||
return InvalidationStatus::Lost;
|
||||
},
|
||||
Some(TransactionStatusOf::<C>::Usurped(new_transaction_hash)) => {
|
||||
// this may be result of our transaction resubmitter work or some manual
|
||||
// intervention. In both cases - let's start stall timeout, because the meaning
|
||||
// of transaction may have changed
|
||||
tracing::trace!(
|
||||
target: "bridge",
|
||||
node=%C::NAME,
|
||||
transaction=?transaction_hash,
|
||||
new_transaction=?new_transaction_hash,
|
||||
"Transaction has been usurped"
|
||||
);
|
||||
return InvalidationStatus::Lost;
|
||||
},
|
||||
Some(TransactionStatusOf::<C>::Dropped) => {
|
||||
// the transaction has been removed from the pool because of its limits. Let's wait
|
||||
// a bit and report a stall
|
||||
tracing::trace!(
|
||||
target: "bridge",
|
||||
node=%C::NAME,
|
||||
transaction=?transaction_hash,
|
||||
"Transaction has been dropped from the pool"
|
||||
);
|
||||
return InvalidationStatus::Lost;
|
||||
},
|
||||
None => {
|
||||
// the status of transaction is unknown to us (the subscription has been closed?).
|
||||
// Let's wait a bit and report a stall
|
||||
return InvalidationStatus::Lost;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{test_chain::TestChain, StreamDescription};
|
||||
use futures::{FutureExt, SinkExt};
|
||||
use pezsc_transaction_pool_api::TransactionStatus;
|
||||
|
||||
struct TestEnvironment(Result<HeaderIdOf<TestChain>, Error>);
|
||||
|
||||
#[async_trait]
|
||||
impl Environment<TestChain> for TestEnvironment {
|
||||
async fn header_id_by_hash(
|
||||
&self,
|
||||
_hash: HashOf<TestChain>,
|
||||
) -> Result<HeaderIdOf<TestChain>, Error> {
|
||||
self.0.as_ref().map_err(|_| Error::BridgePalletIsNotInitialized).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_transaction_status(
|
||||
status: TransactionStatus<HashOf<TestChain>, HashOf<TestChain>>,
|
||||
) -> Option<(
|
||||
TrackedTransactionStatus<HeaderIdOf<TestChain>>,
|
||||
InvalidationStatus<HeaderIdOf<TestChain>>,
|
||||
)> {
|
||||
let (mut sender, receiver) = futures::channel::mpsc::channel(1);
|
||||
let tx_tracker = TransactionTracker::<TestChain, TestEnvironment>::new(
|
||||
TestEnvironment(Ok(HeaderId(0, Default::default()))),
|
||||
Duration::from_secs(0),
|
||||
Default::default(),
|
||||
Subscription::new_forwarded(
|
||||
StreamDescription::new("test".into(), "test".into()),
|
||||
receiver,
|
||||
),
|
||||
);
|
||||
|
||||
// we can't do `.now_or_never()` on `do_wait()` call, because `Subscription` has its own
|
||||
// background thread, which may cause additional async task switches => let's leave some
|
||||
// relatively small timeout here
|
||||
let wait_for_stall_timeout = async_std::task::sleep(std::time::Duration::from_millis(100));
|
||||
let wait_for_stall_timeout_rest = futures::future::ready(());
|
||||
sender.send(Ok(status)).await.unwrap();
|
||||
|
||||
let (ts, is) =
|
||||
tx_tracker.do_wait(wait_for_stall_timeout, wait_for_stall_timeout_rest).await;
|
||||
is.map(|is| (ts, is))
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn returns_finalized_on_finalized() {
|
||||
assert_eq!(
|
||||
on_transaction_status(TransactionStatus::Finalized(Default::default())).await,
|
||||
Some((
|
||||
TrackedTransactionStatus::Finalized(Default::default()),
|
||||
InvalidationStatus::Finalized(Default::default())
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn returns_lost_on_finalized_and_environment_error() {
|
||||
assert_eq!(
|
||||
watch_transaction_status::<_, TestChain, _>(
|
||||
TestEnvironment(Err(Error::BridgePalletIsNotInitialized)),
|
||||
Default::default(),
|
||||
futures::stream::iter([TransactionStatus::Finalized(Default::default())])
|
||||
)
|
||||
.now_or_never(),
|
||||
Some(InvalidationStatus::Lost),
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn returns_invalid_on_invalid() {
|
||||
assert_eq!(
|
||||
on_transaction_status(TransactionStatus::Invalid).await,
|
||||
Some((TrackedTransactionStatus::Lost, InvalidationStatus::Invalid)),
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn waits_on_future() {
|
||||
assert_eq!(on_transaction_status(TransactionStatus::Future).await, None,);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn waits_on_ready() {
|
||||
assert_eq!(on_transaction_status(TransactionStatus::Ready).await, None,);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn waits_on_broadcast() {
|
||||
assert_eq!(
|
||||
on_transaction_status(TransactionStatus::Broadcast(Default::default())).await,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn waits_on_in_block() {
|
||||
assert_eq!(
|
||||
on_transaction_status(TransactionStatus::InBlock(Default::default())).await,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn waits_on_retracted() {
|
||||
assert_eq!(
|
||||
on_transaction_status(TransactionStatus::Retracted(Default::default())).await,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn lost_on_finality_timeout() {
|
||||
assert_eq!(
|
||||
on_transaction_status(TransactionStatus::FinalityTimeout(Default::default())).await,
|
||||
Some((TrackedTransactionStatus::Lost, InvalidationStatus::Lost)),
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn lost_on_usurped() {
|
||||
assert_eq!(
|
||||
on_transaction_status(TransactionStatus::Usurped(Default::default())).await,
|
||||
Some((TrackedTransactionStatus::Lost, InvalidationStatus::Lost)),
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn lost_on_dropped() {
|
||||
assert_eq!(
|
||||
on_transaction_status(TransactionStatus::Dropped).await,
|
||||
Some((TrackedTransactionStatus::Lost, InvalidationStatus::Lost)),
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn lost_on_subscription_error() {
|
||||
assert_eq!(
|
||||
watch_transaction_status::<_, TestChain, _>(
|
||||
TestEnvironment(Ok(HeaderId(0, Default::default()))),
|
||||
Default::default(),
|
||||
futures::stream::iter([])
|
||||
)
|
||||
.now_or_never(),
|
||||
Some(InvalidationStatus::Lost),
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn lost_on_timeout_when_waiting_for_invalidation_status() {
|
||||
let (_sender, receiver) = futures::channel::mpsc::channel(1);
|
||||
let tx_tracker = TransactionTracker::<TestChain, TestEnvironment>::new(
|
||||
TestEnvironment(Ok(HeaderId(0, Default::default()))),
|
||||
Duration::from_secs(0),
|
||||
Default::default(),
|
||||
Subscription::new_forwarded(
|
||||
StreamDescription::new("test".into(), "test".into()),
|
||||
receiver,
|
||||
),
|
||||
);
|
||||
|
||||
let wait_for_stall_timeout = futures::future::ready(()).shared();
|
||||
let wait_for_stall_timeout_rest = wait_for_stall_timeout.clone();
|
||||
let wait_result = tx_tracker
|
||||
.do_wait(wait_for_stall_timeout, wait_for_stall_timeout_rest)
|
||||
.now_or_never();
|
||||
|
||||
assert_eq!(wait_result, Some((TrackedTransactionStatus::Lost, None)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user