From e26d59105acb795161881e49eb14992b8b0c22fa Mon Sep 17 00:00:00 2001 From: fewensa <37804932+fewensa@users.noreply.github.com> Date: Mon, 22 Nov 2021 15:10:40 +0800 Subject: [PATCH] Darwinia feemarket strategy (#8) * Fee market relay strategy * Pick darwinia-v0.11.6-2 --- bin/runtime-common/src/messages.rs | 8 +- .../lib-substrate-relay/src/messages_lane.rs | 11 +- .../src/messages_target.rs | 9 +- relays/messages/Cargo.toml | 2 + relays/messages/src/lib.rs | 1 + relays/messages/src/message_lane.rs | 4 +- relays/messages/src/message_lane_loop.rs | 27 +- relays/messages/src/message_race_delivery.rs | 322 +++--------------- .../src/relay_strategy/altruistic_strategy.rs | 45 +++ .../relay_strategy/enforcement_strategy.rs | 219 ++++++++++++ .../src/relay_strategy/mix_strategy.rs | 40 +++ relays/messages/src/relay_strategy/mod.rs | 123 +++++++ .../src/relay_strategy/rational_strategy.rs | 121 +++++++ 13 files changed, 631 insertions(+), 301 deletions(-) create mode 100644 relays/messages/src/relay_strategy/altruistic_strategy.rs create mode 100644 relays/messages/src/relay_strategy/enforcement_strategy.rs create mode 100644 relays/messages/src/relay_strategy/mix_strategy.rs create mode 100644 relays/messages/src/relay_strategy/mod.rs create mode 100644 relays/messages/src/relay_strategy/rational_strategy.rs diff --git a/bin/runtime-common/src/messages.rs b/bin/runtime-common/src/messages.rs index beb8f30e3..38f0b3e3e 100644 --- a/bin/runtime-common/src/messages.rs +++ b/bin/runtime-common/src/messages.rs @@ -237,9 +237,7 @@ pub mod source { impl Size for FromBridgedChainMessagesDeliveryProof { fn size_hint(&self) -> u32 { u32::try_from( - self.storage_proof - .iter() - .fold(0usize, |sum, node| sum.saturating_add(node.len())), + self.storage_proof.iter().fold(0usize, |sum, node| sum.saturating_add(node.len())), ) .unwrap_or(u32::MAX) } @@ -483,9 +481,7 @@ pub mod target { impl Size for FromBridgedChainMessagesProof { fn size_hint(&self) -> u32 { u32::try_from( - self.storage_proof - .iter() - .fold(0usize, |sum, node| sum.saturating_add(node.len())), + self.storage_proof.iter().fold(0usize, |sum, node| sum.saturating_add(node.len())), ) .unwrap_or(u32::MAX) } diff --git a/relays/lib-substrate-relay/src/messages_lane.rs b/relays/lib-substrate-relay/src/messages_lane.rs index 973f52d60..94cee99f3 100644 --- a/relays/lib-substrate-relay/src/messages_lane.rs +++ b/relays/lib-substrate-relay/src/messages_lane.rs @@ -25,7 +25,10 @@ use async_trait::async_trait; use bp_messages::{LaneId, MessageNonce}; use bp_runtime::{AccountIdOf, IndexOf}; use frame_support::weights::Weight; -use messages_relay::message_lane::{MessageLane, SourceHeaderIdOf, TargetHeaderIdOf}; +use messages_relay::{ + message_lane::{MessageLane, SourceHeaderIdOf, TargetHeaderIdOf}, + relay_strategy::RelayStrategy, +}; use relay_substrate_client::{ metrics::{FloatStorageValueMetric, StorageProofOverheadMetric}, BlockNumberOf, Chain, Client, HashOf, @@ -39,7 +42,7 @@ use sp_runtime::FixedU128; use std::ops::RangeInclusive; /// Substrate <-> Substrate messages relay parameters. -pub struct MessagesRelayParams { +pub struct MessagesRelayParams { /// Messages source client. pub source_client: Client, /// Sign parameters for messages source chain. @@ -54,10 +57,10 @@ pub struct MessagesRelayParams { pub target_to_source_headers_relay: Option>, /// Identifier of lane that needs to be served. pub lane_id: LaneId, - /// Relayer operating mode. - pub relayer_mode: messages_relay::message_lane_loop::RelayerMode, /// Metrics parameters. pub metrics_params: MetricsParams, + /// Relay strategy + pub relay_strategy: Strategy, } /// Message sync pipeline for Substrate <-> Substrate relays. diff --git a/relays/lib-substrate-relay/src/messages_target.rs b/relays/lib-substrate-relay/src/messages_target.rs index f7b911f2c..501ce64d8 100644 --- a/relays/lib-substrate-relay/src/messages_target.rs +++ b/relays/lib-substrate-relay/src/messages_target.rs @@ -202,12 +202,8 @@ where P::MESSAGE_PALLET_NAME_AT_TARGET, &self.lane_id, ); - let proof = self - .client - .prove_storage(vec![inbound_data_key], id.1) - .await? - .iter_nodes() - .collect(); + let proof = + self.client.prove_storage(vec![inbound_data_key], id.1).await?.iter_nodes().collect(); let proof = FromBridgedChainMessagesDeliveryProof { bridged_header_hash: id.1, storage_proof: proof, @@ -434,6 +430,7 @@ fn compute_prepaid_messages_refund( #[cfg(test)] mod tests { use super::*; + use messages_relay::relay_strategy::AltruisticStrategy; use relay_rococo_client::{Rococo, SigningParams as RococoSigningParams}; use relay_wococo_client::{SigningParams as WococoSigningParams, Wococo}; diff --git a/relays/messages/Cargo.toml b/relays/messages/Cargo.toml index 8fc0e7c4f..f9ae555ac 100644 --- a/relays/messages/Cargo.toml +++ b/relays/messages/Cargo.toml @@ -20,3 +20,5 @@ parking_lot = "0.11.0" bp-messages = { path = "../../primitives/messages" } bp-runtime = { path = "../../primitives/runtime" } relay-utils = { path = "../utils" } + +sp-arithmetic = { git = "https://github.com/darwinia-network/substrate", tag = "darwinia-v0.11.6-1" } diff --git a/relays/messages/src/lib.rs b/relays/messages/src/lib.rs index 861091ab2..c9e460300 100644 --- a/relays/messages/src/lib.rs +++ b/relays/messages/src/lib.rs @@ -29,6 +29,7 @@ mod metrics; pub mod message_lane; pub mod message_lane_loop; +pub mod relay_strategy; mod message_race_delivery; mod message_race_loop; diff --git a/relays/messages/src/message_lane.rs b/relays/messages/src/message_lane.rs index 2b2d8029f..5c9728ad9 100644 --- a/relays/messages/src/message_lane.rs +++ b/relays/messages/src/message_lane.rs @@ -21,6 +21,7 @@ use num_traits::{SaturatingAdd, Zero}; use relay_utils::{BlockNumberBase, HeaderId}; +use sp_arithmetic::traits::AtLeast32BitUnsigned; use std::{fmt::Debug, ops::Sub}; /// One-way message lane. @@ -40,7 +41,8 @@ pub trait MessageLane: 'static + Clone + Send + Sync { /// 1) pay transaction fees; /// 2) pay message delivery and dispatch fee; /// 3) pay relayer rewards. - type SourceChainBalance: Clone + type SourceChainBalance: AtLeast32BitUnsigned + + Clone + Copy + Debug + PartialOrd diff --git a/relays/messages/src/message_lane_loop.rs b/relays/messages/src/message_lane_loop.rs index 595d241bf..bb4cd262c 100644 --- a/relays/messages/src/message_lane_loop.rs +++ b/relays/messages/src/message_lane_loop.rs @@ -29,6 +29,7 @@ use crate::{ message_race_delivery::run as run_message_delivery_race, message_race_receiving::run as run_message_receiving_race, metrics::MessageLaneLoopMetrics, + relay_strategy::RelayStrategy, }; use async_trait::async_trait; @@ -46,7 +47,7 @@ use std::{collections::BTreeMap, fmt::Debug, future::Future, ops::RangeInclusive /// Message lane loop configuration params. #[derive(Debug, Clone)] -pub struct Params { +pub struct Params { /// Id of lane this loop is servicing. pub lane: LaneId, /// Interval at which we ask target node about its updates. @@ -58,7 +59,7 @@ pub struct Params { /// The loop will auto-restart if there has been no updates during this period. pub stall_timeout: Duration, /// Message delivery race parameters. - pub delivery_params: MessageDeliveryParams, + pub delivery_params: MessageDeliveryParams, } /// Relayer operating mode. @@ -73,7 +74,7 @@ pub enum RelayerMode { /// Message delivery race parameters. #[derive(Debug, Clone)] -pub struct MessageDeliveryParams { +pub struct MessageDeliveryParams { /// Maximal number of unconfirmed relayer entries at the inbound lane. If there's that number /// of entries in the `InboundLaneData::relayers` set, all new messages will be rejected until /// reward payment will be proved (by including outbound lane state to the message delivery @@ -89,8 +90,8 @@ pub struct MessageDeliveryParams { pub max_messages_weight_in_single_batch: Weight, /// Maximal cumulative size of relayed messages in single delivery transaction. pub max_messages_size_in_single_batch: u32, - /// Relayer operating mode. - pub relayer_mode: RelayerMode, + /// Relay strategy + pub relay_strategy: Strategy, } /// Message details. @@ -257,8 +258,8 @@ pub fn metrics_prefix(lane: &LaneId) -> String { } /// Run message lane service loop. -pub async fn run( - params: Params, +pub async fn run( + params: Params, source_client: impl SourceClient

, target_client: impl TargetClient

, metrics_params: MetricsParams, @@ -286,8 +287,13 @@ pub async fn run( /// Run one-way message delivery loop until connection with target or source node is lost, or exit /// signal is received. -async fn run_until_connection_lost, TC: TargetClient

>( - params: Params, +async fn run_until_connection_lost< + P: MessageLane, + Strategy: RelayStrategy, + SC: SourceClient

, + TC: TargetClient

, +>( + params: Params, source_client: SC, target_client: TC, metrics_msg: Option, @@ -450,6 +456,7 @@ async fn run_until_connection_lost, TC: Targ #[cfg(test)] pub(crate) mod tests { use super::*; + use crate::relay_strategy::AltruisticStrategy; use futures::stream::StreamExt; use parking_lot::Mutex; use relay_utils::{HeaderId, MaybeConnectionError}; @@ -807,7 +814,7 @@ pub(crate) mod tests { max_messages_in_single_batch: 4, max_messages_weight_in_single_batch: 4, max_messages_size_in_single_batch: 4, - relayer_mode: RelayerMode::Altruistic, + relay_strategy: AltruisticStrategy, }, }, source_client, diff --git a/relays/messages/src/message_race_delivery.rs b/relays/messages/src/message_race_delivery.rs index 3433b683d..493128545 100644 --- a/relays/messages/src/message_race_delivery.rs +++ b/relays/messages/src/message_race_delivery.rs @@ -26,6 +26,7 @@ use crate::{ }, message_race_strategy::{BasicStrategy, SourceRangesQueue}, metrics::MessageLaneLoopMetrics, + relay_strategy::{EnforcementStrategy, RelayMessagesBatchReference, RelayStrategy}, }; use async_trait::async_trait; @@ -42,14 +43,14 @@ use std::{ }; /// Run message delivery race. -pub async fn run( +pub async fn run( source_client: impl MessageLaneSourceClient

, source_state_updates: impl FusedStream>, target_client: impl MessageLaneTargetClient

, target_state_updates: impl FusedStream>, stall_timeout: Duration, metrics_msg: Option, - params: MessageDeliveryParams, + params: MessageDeliveryParams, ) -> Result<(), FailedClient> { crate::message_race_loop::run( MessageDeliveryRaceSource { @@ -65,7 +66,7 @@ pub async fn run( }, target_state_updates, stall_timeout, - MessageDeliveryStrategy:: { + MessageDeliveryStrategy:: { lane_source_client: source_client, lane_target_client: target_client, max_unrewarded_relayer_entries_at_target: params @@ -74,7 +75,7 @@ pub async fn run( max_messages_in_single_batch: params.max_messages_in_single_batch, max_messages_weight_in_single_batch: params.max_messages_weight_in_single_batch, max_messages_size_in_single_batch: params.max_messages_size_in_single_batch, - relayer_mode: params.relayer_mode, + relay_strategy: params.relay_strategy, latest_confirmed_nonces_at_source: VecDeque::new(), target_nonces: None, strategy: BasicStrategy::new(), @@ -235,7 +236,7 @@ struct DeliveryRaceTargetNoncesData { } /// Messages delivery strategy. -struct MessageDeliveryStrategy { +struct MessageDeliveryStrategy { /// The client that is connected to the message lane source node. lane_source_client: SC, /// The client that is connected to the message lane target node. @@ -251,7 +252,7 @@ struct MessageDeliveryStrategy { /// Maximal messages size in the single delivery transaction. max_messages_size_in_single_batch: u32, /// Relayer operating mode. - relayer_mode: RelayerMode, + relay_strategy: Strategy, /// Latest confirmed nonces at the source client + the header id where we have first met this /// nonce. latest_confirmed_nonces_at_source: VecDeque<(SourceHeaderIdOf

, MessageNonce)>, @@ -270,7 +271,9 @@ type MessageDeliveryStrategyBase

= BasicStrategy<

::MessagesProof, >; -impl std::fmt::Debug for MessageDeliveryStrategy { +impl std::fmt::Debug + for MessageDeliveryStrategy +{ fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { fmt.debug_struct("MessageDeliveryStrategy") .field( @@ -288,7 +291,7 @@ impl std::fmt::Debug for MessageDeliveryStrategy MessageDeliveryStrategy { +impl MessageDeliveryStrategy { /// Returns total weight of all undelivered messages. fn total_queued_dispatch_weight(&self) -> Weight { self.strategy @@ -300,8 +303,9 @@ impl MessageDeliveryStrategy { } #[async_trait] -impl RaceStrategy, TargetHeaderIdOf

, P::MessagesProof> - for MessageDeliveryStrategy +impl + RaceStrategy, TargetHeaderIdOf

, P::MessagesProof> + for MessageDeliveryStrategy where P: MessageLane, SC: MessageLaneSourceClient

, @@ -504,7 +508,6 @@ where let max_nonces = std::cmp::min(max_nonces, self.max_messages_in_single_batch); let max_messages_weight_in_single_batch = self.max_messages_weight_in_single_batch; let max_messages_size_in_single_batch = self.max_messages_size_in_single_batch; - let relayer_mode = self.relayer_mode; let lane_source_client = self.lane_source_client.clone(); let lane_target_client = self.lane_target_client.clone(); @@ -512,17 +515,19 @@ where self.strategy.maximal_available_source_queue_index(race_state)?; let previous_total_dispatch_weight = self.total_queued_dispatch_weight(); let source_queue = self.strategy.source_queue(); - let range_end = select_nonces_for_delivery_transaction( - relayer_mode, - max_nonces, + + let reference = RelayMessagesBatchReference { + max_messages_in_this_batch: max_nonces, max_messages_weight_in_single_batch, max_messages_size_in_single_batch, - lane_source_client.clone(), - lane_target_client.clone(), - source_queue, - 0..maximal_source_queue_index + 1, - ) - .await?; + lane_source_client: lane_source_client.clone(), + lane_target_client: lane_target_client.clone(), + nonces_queue: source_queue.clone(), + nonces_queue_range: 0..maximal_source_queue_index + 1, + }; + + let strategy = EnforcementStrategy::new(self.relay_strategy.clone()); + let range_end = strategy.decide(reference).await?; let range_begin = source_queue[0].1.begin(); let selected_nonces = range_begin..=range_end; @@ -538,236 +543,6 @@ where } } -/// From given set of source nonces, that are ready to be delivered, select nonces -/// to fit into single delivery transaction. -/// -/// The function returns last nonce that must be delivered to the target chain. -#[allow(clippy::too_many_arguments)] -async fn select_nonces_for_delivery_transaction( - relayer_mode: RelayerMode, - max_messages_in_this_batch: MessageNonce, - max_messages_weight_in_single_batch: Weight, - max_messages_size_in_single_batch: u32, - lane_source_client: impl MessageLaneSourceClient

, - lane_target_client: impl MessageLaneTargetClient

, - nonces_queue: &SourceRangesQueue< - P::SourceHeaderHash, - P::SourceHeaderNumber, - MessageDetailsMap, - >, - nonces_queue_range: Range, -) -> Option { - let mut hard_selected_count = 0; - let mut soft_selected_count = 0; - - let mut selected_weight: Weight = 0; - let mut selected_unpaid_weight: Weight = 0; - let mut selected_prepaid_nonces = 0; - let mut selected_size: u32 = 0; - let mut selected_count: MessageNonce = 0; - let mut selected_reward = P::SourceChainBalance::zero(); - let mut selected_cost = P::SourceChainBalance::zero(); - - let mut total_reward = P::SourceChainBalance::zero(); - let mut total_confirmations_cost = P::SourceChainBalance::zero(); - let mut total_cost = P::SourceChainBalance::zero(); - - let hard_selected_begin_nonce = nonces_queue[nonces_queue_range.start].1.begin(); - - // technically, multiple confirmations will be delivered in a single transaction, - // meaning less loses for relayer. But here we don't know the final relayer yet, so - // we're adding a separate transaction for every message. Normally, this cost is covered - // by the message sender. Probably reconsider this? - let confirmation_transaction_cost = if relayer_mode != RelayerMode::Altruistic { - lane_source_client.estimate_confirmation_transaction().await - } else { - Zero::zero() - }; - - let all_ready_nonces = nonces_queue - .range(nonces_queue_range.clone()) - .flat_map(|(_, ready_nonces)| ready_nonces.iter()) - .enumerate(); - for (index, (nonce, details)) in all_ready_nonces { - // Since we (hopefully) have some reserves in `max_messages_weight_in_single_batch` - // and `max_messages_size_in_single_batch`, we may still try to submit transaction - // with single message if message overflows these limits. The worst case would be if - // transaction will be rejected by the target runtime, but at least we have tried. - - // limit messages in the batch by weight - let new_selected_weight = match selected_weight.checked_add(details.dispatch_weight) { - Some(new_selected_weight) - if new_selected_weight <= max_messages_weight_in_single_batch => - new_selected_weight, - new_selected_weight if selected_count == 0 => { - log::warn!( - target: "bridge", - "Going to submit message delivery transaction with declared dispatch \ - weight {:?} that overflows maximal configured weight {}", - new_selected_weight, - max_messages_weight_in_single_batch, - ); - new_selected_weight.unwrap_or(Weight::MAX) - }, - _ => break, - }; - - // limit messages in the batch by size - let new_selected_size = match selected_size.checked_add(details.size) { - Some(new_selected_size) if new_selected_size <= max_messages_size_in_single_batch => - new_selected_size, - new_selected_size if selected_count == 0 => { - log::warn!( - target: "bridge", - "Going to submit message delivery transaction with message \ - size {:?} that overflows maximal configured size {}", - new_selected_size, - max_messages_size_in_single_batch, - ); - new_selected_size.unwrap_or(u32::MAX) - }, - _ => break, - }; - - // limit number of messages in the batch - let new_selected_count = selected_count + 1; - if new_selected_count > max_messages_in_this_batch { - break - } - - // If dispatch fee has been paid at the source chain, it means that it is **relayer** who's - // paying for dispatch at the target chain AND reward must cover this dispatch fee. - // - // If dispatch fee is paid at the target chain, it means that it'll be withdrawn from the - // dispatch origin account AND reward is not covering this fee. - // - // So in the latter case we're not adding the dispatch weight to the delivery transaction - // weight. - let mut new_selected_prepaid_nonces = selected_prepaid_nonces; - let new_selected_unpaid_weight = match details.dispatch_fee_payment { - DispatchFeePayment::AtSourceChain => { - new_selected_prepaid_nonces += 1; - selected_unpaid_weight.saturating_add(details.dispatch_weight) - }, - DispatchFeePayment::AtTargetChain => selected_unpaid_weight, - }; - - // now the message has passed all 'strong' checks, and we CAN deliver it. But do we WANT - // to deliver it? It depends on the relayer strategy. - match relayer_mode { - RelayerMode::Altruistic => { - soft_selected_count = index + 1; - }, - RelayerMode::Rational => { - let delivery_transaction_cost = lane_target_client - .estimate_delivery_transaction_in_source_tokens( - hard_selected_begin_nonce..= - (hard_selected_begin_nonce + index as MessageNonce), - new_selected_prepaid_nonces, - new_selected_unpaid_weight, - new_selected_size as u32, - ) - .await - .map_err(|err| { - log::debug!( - target: "bridge", - "Failed to estimate delivery transaction cost: {:?}. No nonces selected for delivery", - err, - ); - }) - .ok()?; - - // if it is the first message that makes reward less than cost, let's log it - // if this message makes batch profitable again, let's log it - let is_total_reward_less_than_cost = total_reward < total_cost; - let prev_total_cost = total_cost; - let prev_total_reward = total_reward; - total_confirmations_cost = - total_confirmations_cost.saturating_add(&confirmation_transaction_cost); - total_reward = total_reward.saturating_add(&details.reward); - total_cost = total_confirmations_cost.saturating_add(&delivery_transaction_cost); - if !is_total_reward_less_than_cost && total_reward < total_cost { - log::debug!( - target: "bridge", - "Message with nonce {} (reward = {:?}) changes total cost {:?}->{:?} and makes it larger than \ - total reward {:?}->{:?}", - nonce, - details.reward, - prev_total_cost, - total_cost, - prev_total_reward, - total_reward, - ); - } else if is_total_reward_less_than_cost && total_reward >= total_cost { - log::debug!( - target: "bridge", - "Message with nonce {} (reward = {:?}) changes total cost {:?}->{:?} and makes it less than or \ - equal to the total reward {:?}->{:?} (again)", - nonce, - details.reward, - prev_total_cost, - total_cost, - prev_total_reward, - total_reward, - ); - } - - // Rational relayer never want to lose his funds - if total_reward >= total_cost { - soft_selected_count = index + 1; - selected_reward = total_reward; - selected_cost = total_cost; - } - }, - } - - hard_selected_count = index + 1; - selected_weight = new_selected_weight; - selected_unpaid_weight = new_selected_unpaid_weight; - selected_prepaid_nonces = new_selected_prepaid_nonces; - selected_size = new_selected_size; - selected_count = new_selected_count; - } - - if hard_selected_count != soft_selected_count { - let hard_selected_end_nonce = - hard_selected_begin_nonce + hard_selected_count as MessageNonce - 1; - let soft_selected_begin_nonce = hard_selected_begin_nonce; - let soft_selected_end_nonce = - soft_selected_begin_nonce + soft_selected_count as MessageNonce - 1; - log::warn!( - target: "bridge", - "Relayer may deliver nonces [{:?}; {:?}], but because of its strategy ({:?}) it has selected \ - nonces [{:?}; {:?}].", - hard_selected_begin_nonce, - hard_selected_end_nonce, - relayer_mode, - soft_selected_begin_nonce, - soft_selected_end_nonce, - ); - - hard_selected_count = soft_selected_count; - } - - if hard_selected_count != 0 { - if relayer_mode != RelayerMode::Altruistic { - log::trace!( - target: "bridge", - "Expected reward from delivering nonces [{:?}; {:?}] is: {:?} - {:?} = {:?}", - hard_selected_begin_nonce, - hard_selected_begin_nonce + hard_selected_count as MessageNonce - 1, - selected_reward, - selected_cost, - selected_reward - selected_cost, - ); - } - - Some(hard_selected_begin_nonce + hard_selected_count as MessageNonce - 1) - } else { - None - } -} - impl NoncesRange for MessageDetailsMap { fn begin(&self) -> MessageNonce { self.keys().next().cloned().unwrap_or_default() @@ -790,15 +565,18 @@ impl NoncesRange for MessageDetailsMap; type TestStrategy = - MessageDeliveryStrategy; + MessageDeliveryStrategy; fn source_nonces( new_nonces: RangeInclusive, @@ -848,7 +626,6 @@ mod tests { }; let mut race_strategy = TestStrategy { - relayer_mode: RelayerMode::Altruistic, max_unrewarded_relayer_entries_at_target: 4, max_unconfirmed_nonces_at_target: 4, max_messages_in_single_batch: 4, @@ -869,20 +646,17 @@ mod tests { }, }), strategy: BasicStrategy::new(), + relay_strategy: MixStrategy::new(RelayerMode::Altruistic), }; race_strategy.strategy.source_nonces_updated( header_id(1), - source_nonces(20..=23, 19, DEFAULT_REWARD, AtSourceChain), + source_nonces(20..=23, 19, DEFAULT_REWARD, DispatchFeePayment::AtSourceChain), ); let target_nonces = TargetClientNonces { latest_nonce: 19, nonces_data: () }; - race_strategy - .strategy - .best_target_nonces_updated(target_nonces.clone(), &mut race_state); - race_strategy - .strategy - .finalized_target_nonces_updated(target_nonces, &mut race_state); + race_strategy.strategy.best_target_nonces_updated(target_nonces.clone(), &mut race_state); + race_strategy.strategy.finalized_target_nonces_updated(target_nonces, &mut race_state); (race_state, race_strategy) } @@ -907,7 +681,7 @@ mod tests { dispatch_weight: idx, size: idx as _, reward: idx as _, - dispatch_fee_payment: AtSourceChain, + dispatch_fee_payment: DispatchFeePayment::AtSourceChain, }, ) }) @@ -1199,7 +973,7 @@ mod tests { #[async_std::test] async fn rational_relayer_is_delivering_messages_if_cost_is_equal_to_reward() { let (state, mut strategy) = prepare_strategy(); - strategy.relayer_mode = RelayerMode::Rational; + strategy.relay_strategy = MixStrategy::new(RelayerMode::Rational); // so now we have: // - 20..=23 with reward = cost @@ -1217,11 +991,11 @@ mod tests { 24..=25, 19, DEFAULT_REWARD - BASE_MESSAGE_DELIVERY_TRANSACTION_COST, - AtSourceChain, + DispatchFeePayment::AtSourceChain, ); strategy.strategy.source_nonces_updated(header_id(2), nonces); state.best_finalized_source_header_id_at_best_target = Some(header_id(2)); - strategy.relayer_mode = RelayerMode::Rational; + strategy.relay_strategy = MixStrategy::new(RelayerMode::Rational); // so now we have: // - 20..=23 with reward = cost @@ -1252,7 +1026,7 @@ mod tests { strategy.max_messages_in_single_batch = 100; strategy.max_messages_weight_in_single_batch = 100; strategy.max_messages_size_in_single_batch = 100; - strategy.relayer_mode = RelayerMode::Rational; + strategy.relay_strategy = MixStrategy::new(RelayerMode::Rational); // so now we have: // - 20..=23 with reward = cost @@ -1264,11 +1038,11 @@ mod tests { } assert_eq!( - test_with_dispatch_fee_payment(AtTargetChain).await, + test_with_dispatch_fee_payment(DispatchFeePayment::AtTargetChain).await, Some(((20..=24), proof_parameters(false, 5))) ); assert_eq!( - test_with_dispatch_fee_payment(AtSourceChain).await, + test_with_dispatch_fee_payment(DispatchFeePayment::AtSourceChain).await, Some(((20..=23), proof_parameters(false, 4))) ); } @@ -1284,7 +1058,7 @@ mod tests { // This was happening because selector (`select_nonces_for_delivery_transaction`) has been // called for every `source_queue` entry separately without preserving any context. let (mut state, mut strategy) = prepare_strategy(); - let nonces = source_nonces(24..=25, 19, DEFAULT_REWARD, AtSourceChain); + let nonces = source_nonces(24..=25, 19, DEFAULT_REWARD, DispatchFeePayment::AtSourceChain); strategy.strategy.source_nonces_updated(header_id(2), nonces); strategy.max_unrewarded_relayer_entries_at_target = 100; strategy.max_unconfirmed_nonces_at_target = 100; diff --git a/relays/messages/src/relay_strategy/altruistic_strategy.rs b/relays/messages/src/relay_strategy/altruistic_strategy.rs new file mode 100644 index 000000000..f932b796b --- /dev/null +++ b/relays/messages/src/relay_strategy/altruistic_strategy.rs @@ -0,0 +1,45 @@ +// 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 . + +//! Altruistic relay strategy + +use async_trait::async_trait; + +use crate::{ + message_lane::MessageLane, + message_lane_loop::{ + SourceClient as MessageLaneSourceClient, TargetClient as MessageLaneTargetClient, + }, + relay_strategy::{RelayReference, RelayStrategy}, +}; + +/// The relayer doesn't care about rewards. +#[derive(Clone)] +pub struct AltruisticStrategy; + +#[async_trait] +impl RelayStrategy for AltruisticStrategy { + async fn decide< + P: MessageLane, + SourceClient: MessageLaneSourceClient

, + TargetClient: MessageLaneTargetClient

, + >( + &self, + _reference: &mut RelayReference, + ) -> bool { + true + } +} diff --git a/relays/messages/src/relay_strategy/enforcement_strategy.rs b/relays/messages/src/relay_strategy/enforcement_strategy.rs new file mode 100644 index 000000000..042c05bec --- /dev/null +++ b/relays/messages/src/relay_strategy/enforcement_strategy.rs @@ -0,0 +1,219 @@ +// 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 . + +//! enforcement strategy + +use num_traits::Zero; + +use bp_messages::{MessageNonce, Weight}; +use bp_runtime::messages::DispatchFeePayment; + +use crate::{ + message_lane::MessageLane, + message_lane_loop::{ + MessageDetails, SourceClient as MessageLaneSourceClient, + TargetClient as MessageLaneTargetClient, + }, + message_race_loop::NoncesRange, + relay_strategy::{RelayMessagesBatchReference, RelayReference, RelayStrategy}, +}; + +/// Do hard check and run soft check strategy +#[derive(Clone)] +pub struct EnforcementStrategy { + strategy: Strategy, +} + +impl EnforcementStrategy { + pub fn new(strategy: Strategy) -> Self { + Self { strategy } + } +} + +impl EnforcementStrategy { + pub async fn decide< + P: MessageLane, + SourceClient: MessageLaneSourceClient

, + TargetClient: MessageLaneTargetClient

, + >( + &self, + reference: RelayMessagesBatchReference, + ) -> Option { + let mut hard_selected_count = 0; + let mut soft_selected_count = 0; + + let mut selected_weight: Weight = 0; + let mut selected_count: MessageNonce = 0; + + let hard_selected_begin_nonce = + reference.nonces_queue[reference.nonces_queue_range.start].1.begin(); + + // relay reference + let mut relay_reference = RelayReference { + lane_source_client: reference.lane_source_client.clone(), + lane_target_client: reference.lane_target_client.clone(), + + selected_reward: P::SourceChainBalance::zero(), + selected_cost: P::SourceChainBalance::zero(), + selected_size: 0, + + total_reward: P::SourceChainBalance::zero(), + total_confirmations_cost: P::SourceChainBalance::zero(), + total_cost: P::SourceChainBalance::zero(), + + hard_selected_begin_nonce, + selected_prepaid_nonces: 0, + selected_unpaid_weight: 0, + + index: 0, + nonce: 0, + details: MessageDetails { + dispatch_weight: 0, + size: 0, + reward: P::SourceChainBalance::zero(), + dispatch_fee_payment: DispatchFeePayment::AtSourceChain, + }, + }; + + let all_ready_nonces = reference + .nonces_queue + .range(reference.nonces_queue_range.clone()) + .flat_map(|(_, ready_nonces)| ready_nonces.iter()) + .enumerate(); + for (index, (nonce, details)) in all_ready_nonces { + relay_reference.index = index; + relay_reference.nonce = *nonce; + relay_reference.details = *details; + + // Since we (hopefully) have some reserves in `max_messages_weight_in_single_batch` + // and `max_messages_size_in_single_batch`, we may still try to submit transaction + // with single message if message overflows these limits. The worst case would be if + // transaction will be rejected by the target runtime, but at least we have tried. + + // limit messages in the batch by weight + let new_selected_weight = match selected_weight.checked_add(details.dispatch_weight) { + Some(new_selected_weight) + if new_selected_weight <= reference.max_messages_weight_in_single_batch => + new_selected_weight, + new_selected_weight if selected_count == 0 => { + log::warn!( + target: "bridge", + "Going to submit message delivery transaction with declared dispatch \ + weight {:?} that overflows maximal configured weight {}", + new_selected_weight, + reference.max_messages_weight_in_single_batch, + ); + new_selected_weight.unwrap_or(Weight::MAX) + }, + _ => break, + }; + + // limit messages in the batch by size + let new_selected_size = match relay_reference.selected_size.checked_add(details.size) { + Some(new_selected_size) + if new_selected_size <= reference.max_messages_size_in_single_batch => + new_selected_size, + new_selected_size if selected_count == 0 => { + log::warn!( + target: "bridge", + "Going to submit message delivery transaction with message \ + size {:?} that overflows maximal configured size {}", + new_selected_size, + reference.max_messages_size_in_single_batch, + ); + new_selected_size.unwrap_or(u32::MAX) + }, + _ => break, + }; + + // limit number of messages in the batch + let new_selected_count = selected_count + 1; + if new_selected_count > reference.max_messages_in_this_batch { + break + } + relay_reference.selected_size = new_selected_size; + + // If dispatch fee has been paid at the source chain, it means that it is **relayer** + // who's paying for dispatch at the target chain AND reward must cover this dispatch + // fee. + // + // If dispatch fee is paid at the target chain, it means that it'll be withdrawn from + // the dispatch origin account AND reward is not covering this fee. + // + // So in the latter case we're not adding the dispatch weight to the delivery + // transaction weight. + let mut new_selected_prepaid_nonces = relay_reference.selected_prepaid_nonces; + let new_selected_unpaid_weight = match details.dispatch_fee_payment { + DispatchFeePayment::AtSourceChain => { + new_selected_prepaid_nonces += 1; + relay_reference.selected_unpaid_weight.saturating_add(details.dispatch_weight) + }, + DispatchFeePayment::AtTargetChain => relay_reference.selected_unpaid_weight, + }; + relay_reference.selected_prepaid_nonces = new_selected_prepaid_nonces; + relay_reference.selected_unpaid_weight = new_selected_unpaid_weight; + + // now the message has passed all 'strong' checks, and we CAN deliver it. But do we WANT + // to deliver it? It depends on the relayer strategy. + if self.strategy.decide(&mut relay_reference).await { + soft_selected_count = index + 1; + } + + hard_selected_count = index + 1; + selected_weight = new_selected_weight; + selected_count = new_selected_count; + } + + if hard_selected_count != soft_selected_count { + let hard_selected_end_nonce = + hard_selected_begin_nonce + hard_selected_count as MessageNonce - 1; + let soft_selected_begin_nonce = hard_selected_begin_nonce; + let soft_selected_end_nonce = + soft_selected_begin_nonce + soft_selected_count as MessageNonce - 1; + log::warn!( + target: "bridge", + "Relayer may deliver nonces [{:?}; {:?}], but because of its strategy it has selected \ + nonces [{:?}; {:?}].", + hard_selected_begin_nonce, + hard_selected_end_nonce, + soft_selected_begin_nonce, + soft_selected_end_nonce, + ); + + hard_selected_count = soft_selected_count; + } + + if hard_selected_count != 0 { + if relay_reference.selected_reward != P::SourceChainBalance::zero() && + relay_reference.selected_cost != P::SourceChainBalance::zero() + { + log::trace!( + target: "bridge", + "Expected reward from delivering nonces [{:?}; {:?}] is: {:?} - {:?} = {:?}", + hard_selected_begin_nonce, + hard_selected_begin_nonce + hard_selected_count as MessageNonce - 1, + &relay_reference.selected_reward, + &relay_reference.selected_cost, + relay_reference.selected_reward - relay_reference.selected_cost, + ); + } + + Some(hard_selected_begin_nonce + hard_selected_count as MessageNonce - 1) + } else { + None + } + } +} diff --git a/relays/messages/src/relay_strategy/mix_strategy.rs b/relays/messages/src/relay_strategy/mix_strategy.rs new file mode 100644 index 000000000..ef472e9c3 --- /dev/null +++ b/relays/messages/src/relay_strategy/mix_strategy.rs @@ -0,0 +1,40 @@ +use async_trait::async_trait; + +use crate::{ + message_lane::MessageLane, + message_lane_loop::{ + RelayerMode, SourceClient as MessageLaneSourceClient, + TargetClient as MessageLaneTargetClient, + }, + relay_strategy::{AltruisticStrategy, RationalStrategy, RelayReference, RelayStrategy}, +}; + +/// The relayer doesn't care about rewards. +#[derive(Clone)] +pub struct MixStrategy { + relayer_mode: RelayerMode, +} + +impl MixStrategy { + /// Create mix strategy instance + pub fn new(relayer_mode: RelayerMode) -> Self { + Self { relayer_mode } + } +} + +#[async_trait] +impl RelayStrategy for MixStrategy { + async fn decide< + P: MessageLane, + SourceClient: MessageLaneSourceClient

, + TargetClient: MessageLaneTargetClient

, + >( + &self, + reference: &mut RelayReference, + ) -> bool { + match self.relayer_mode { + RelayerMode::Altruistic => AltruisticStrategy.decide(reference).await, + RelayerMode::Rational => RationalStrategy.decide(reference).await, + } + } +} diff --git a/relays/messages/src/relay_strategy/mod.rs b/relays/messages/src/relay_strategy/mod.rs new file mode 100644 index 000000000..3e4eef897 --- /dev/null +++ b/relays/messages/src/relay_strategy/mod.rs @@ -0,0 +1,123 @@ +// 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 . + +//! Relayer strategy + +use std::ops::Range; + +use async_trait::async_trait; + +use bp_messages::{MessageNonce, Weight}; + +use crate::{ + message_lane::MessageLane, + message_lane_loop::{ + MessageDetails, MessageDetailsMap, SourceClient as MessageLaneSourceClient, + TargetClient as MessageLaneTargetClient, + }, + message_race_strategy::SourceRangesQueue, +}; + +pub(crate) use self::enforcement_strategy::*; +pub use self::{altruistic_strategy::*, mix_strategy::*, rational_strategy::*}; + +mod altruistic_strategy; +mod enforcement_strategy; +mod mix_strategy; +mod rational_strategy; + +/// Relayer strategy trait +#[async_trait] +pub trait RelayStrategy: 'static + Clone + Send + Sync { + /// The relayer decide how to process nonce by reference. + /// From given set of source nonces, that are ready to be delivered, select nonces + /// to fit into single delivery transaction. + /// + /// The function returns last nonce that must be delivered to the target chain. + async fn decide< + P: MessageLane, + SourceClient: MessageLaneSourceClient

, + TargetClient: MessageLaneTargetClient

, + >( + &self, + reference: &mut RelayReference, + ) -> bool; +} + +/// Reference data for participating in relay +pub struct RelayReference< + P: MessageLane, + SourceClient: MessageLaneSourceClient

, + TargetClient: MessageLaneTargetClient

, +> { + /// The client that is connected to the message lane source node. + pub lane_source_client: SourceClient, + /// The client that is connected to the message lane target node. + pub lane_target_client: TargetClient, + /// Current block reward summary + pub selected_reward: P::SourceChainBalance, + /// Current block cost summary + pub selected_cost: P::SourceChainBalance, + /// Messages size summary + pub selected_size: u32, + + /// Current block reward summary + pub total_reward: P::SourceChainBalance, + /// All confirmations cost + pub total_confirmations_cost: P::SourceChainBalance, + /// Current block cost summary + pub total_cost: P::SourceChainBalance, + + /// Hard check begin nonce + pub hard_selected_begin_nonce: MessageNonce, + /// Count prepaid nonces + pub selected_prepaid_nonces: MessageNonce, + /// Unpaid nonces weight summary + pub selected_unpaid_weight: Weight, + + /// Index by all ready nonces + pub index: usize, + /// Current nonce + pub nonce: MessageNonce, + /// Current nonce details + pub details: MessageDetails, +} + +/// Relay reference data +pub struct RelayMessagesBatchReference< + P: MessageLane, + SourceClient: MessageLaneSourceClient

, + TargetClient: MessageLaneTargetClient

, +> { + /// Maximal number of relayed messages in single delivery transaction. + pub max_messages_in_this_batch: MessageNonce, + /// Maximal cumulative dispatch weight of relayed messages in single delivery transaction. + pub max_messages_weight_in_single_batch: Weight, + /// Maximal cumulative size of relayed messages in single delivery transaction. + pub max_messages_size_in_single_batch: u32, + /// The client that is connected to the message lane source node. + pub lane_source_client: SourceClient, + /// The client that is connected to the message lane target node. + pub lane_target_client: TargetClient, + /// Source queue. + pub nonces_queue: SourceRangesQueue< + P::SourceHeaderHash, + P::SourceHeaderNumber, + MessageDetailsMap, + >, + /// Source queue range + pub nonces_queue_range: Range, +} diff --git a/relays/messages/src/relay_strategy/rational_strategy.rs b/relays/messages/src/relay_strategy/rational_strategy.rs new file mode 100644 index 000000000..5f1917405 --- /dev/null +++ b/relays/messages/src/relay_strategy/rational_strategy.rs @@ -0,0 +1,121 @@ +// 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 . + +//! Rational relay strategy + +use async_trait::async_trait; +use num_traits::SaturatingAdd; + +use bp_messages::MessageNonce; + +use crate::{ + message_lane::MessageLane, + message_lane_loop::{ + SourceClient as MessageLaneSourceClient, TargetClient as MessageLaneTargetClient, + }, + relay_strategy::{RelayReference, RelayStrategy}, +}; + +/// The relayer will deliver all messages and confirmations as long as he's not losing any +/// funds. +#[derive(Clone)] +pub struct RationalStrategy; + +#[async_trait] +impl RelayStrategy for RationalStrategy { + async fn decide< + P: MessageLane, + SourceClient: MessageLaneSourceClient

, + TargetClient: MessageLaneTargetClient

, + >( + &self, + reference: &mut RelayReference, + ) -> bool { + // technically, multiple confirmations will be delivered in a single transaction, + // meaning less loses for relayer. But here we don't know the final relayer yet, so + // we're adding a separate transaction for every message. Normally, this cost is covered + // by the message sender. Probably reconsider this? + let confirmation_transaction_cost = + reference.lane_source_client.estimate_confirmation_transaction().await; + + let delivery_transaction_cost = match reference + .lane_target_client + .estimate_delivery_transaction_in_source_tokens( + reference.hard_selected_begin_nonce..= + (reference.hard_selected_begin_nonce + reference.index as MessageNonce), + reference.selected_prepaid_nonces, + reference.selected_unpaid_weight, + reference.selected_size as u32, + ) + .await + { + Ok(v) => v, + Err(err) => { + log::debug!( + target: "bridge", + "Failed to estimate delivery transaction cost: {:?}. No nonces selected for delivery", + err, + ); + return false + }, + }; + + // if it is the first message that makes reward less than cost, let's log it + // if this message makes batch profitable again, let's log it + let is_total_reward_less_than_cost = reference.total_reward < reference.total_cost; + let prev_total_cost = reference.total_cost; + let prev_total_reward = reference.total_reward; + reference.total_confirmations_cost = + reference.total_confirmations_cost.saturating_add(&confirmation_transaction_cost); + reference.total_reward = reference.total_reward.saturating_add(&reference.details.reward); + reference.total_cost = + reference.total_confirmations_cost.saturating_add(&delivery_transaction_cost); + if !is_total_reward_less_than_cost && reference.total_reward < reference.total_cost { + log::debug!( + target: "bridge", + "Message with nonce {} (reward = {:?}) changes total cost {:?}->{:?} and makes it larger than \ + total reward {:?}->{:?}", + reference.nonce, + reference.details.reward, + prev_total_cost, + reference.total_cost, + prev_total_reward, + reference.total_reward, + ); + } else if is_total_reward_less_than_cost && reference.total_reward >= reference.total_cost { + log::debug!( + target: "bridge", + "Message with nonce {} (reward = {:?}) changes total cost {:?}->{:?} and makes it less than or \ + equal to the total reward {:?}->{:?} (again)", + reference.nonce, + reference.details.reward, + prev_total_cost, + reference.total_cost, + prev_total_reward, + reference.total_reward, + ); + } + + // Rational relayer never want to lose his funds + if reference.total_reward >= reference.total_cost { + reference.selected_reward = reference.total_reward; + reference.selected_cost = reference.total_cost; + return true + } + + false + } +}