diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d321bc6a9..8c549b53b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ - Consistent identifier handling across ICS 02, 03 and 04 ([#622]) - [ibc-relayer] - - [nothing yet] + - Use a stateless light client without a runtime ([#673]) - [ibc-relayer-cli] - Added `create connection` and `create channel` CLIs ([#630], [#715]) @@ -72,6 +72,7 @@ [#599]: https://github.com/informalsystems/ibc-rs/issues/599 [#630]: https://github.com/informalsystems/ibc-rs/issues/630 [#672]: https://github.com/informalsystems/ibc-rs/issues/672 +[#673]: https://github.com/informalsystems/ibc-rs/issues/673 [#685]: https://github.com/informalsystems/ibc-rs/issues/685 [#689]: https://github.com/informalsystems/ibc-rs/issues/689 [#695]: https://github.com/informalsystems/ibc-rs/issues/695 diff --git a/relayer/src/chain.rs b/relayer/src/chain.rs index 0b179f2853..32595ef543 100644 --- a/relayer/src/chain.rs +++ b/relayer/src/chain.rs @@ -80,9 +80,7 @@ pub trait Chain: Sized { #[allow(clippy::type_complexity)] /// Initializes and returns the light client (if any) associated with this chain. - fn init_light_client( - &self, - ) -> Result<(Box>, Option>), Error>; + fn init_light_client(&self) -> Result>, Error>; /// Initializes and returns the event monitor (if any) associated with this chain. fn init_event_monitor( diff --git a/relayer/src/chain/cosmos.rs b/relayer/src/chain/cosmos.rs index 4e6b250d56..dce44693fd 100644 --- a/relayer/src/chain/cosmos.rs +++ b/relayer/src/chain/cosmos.rs @@ -335,18 +335,11 @@ impl Chain for CosmosSdkChain { }) } - // TODO use a simpler approach to create the light client - #[allow(clippy::type_complexity)] - fn init_light_client( - &self, - ) -> Result<(Box>, Option>), Error> { + fn init_light_client(&self) -> Result>, Error> { crate::time!("init_light_client"); - let (lc, supervisor) = TMLightClient::from_config(&self.config, true)?; - - let supervisor_thread = thread::spawn(move || supervisor.run().unwrap()); - - Ok((Box::new(lc), Some(supervisor_thread))) + let light_client = TMLightClient::from_config(&self.config)?; + Ok(Box::new(light_client)) } fn init_event_monitor( @@ -461,7 +454,7 @@ impl Chain for CosmosSdkChain { if status.sync_info.catching_up { fail!( - Kind::LightClientSupervisor(self.config.id.clone()), + Kind::LightClient(self.config.rpc_addr.to_string()), "node at {} running chain {} not caught up", self.config().rpc_addr, self.config().id, diff --git a/relayer/src/chain/handle.rs b/relayer/src/chain/handle.rs index ef5c92e6c4..0fba1f3fbd 100644 --- a/relayer/src/chain/handle.rs +++ b/relayer/src/chain/handle.rs @@ -61,12 +61,6 @@ pub enum ChainRequest { reply_to: ReplyTo>, }, - GetMinimalSet { - from: Height, - to: Height, - reply_to: ReplyTo>, - }, - Signer { reply_to: ReplyTo, }, @@ -87,6 +81,7 @@ pub enum ChainRequest { BuildHeader { trusted_height: Height, target_height: Height, + client_state: AnyClientState, reply_to: ReplyTo, }, @@ -96,7 +91,9 @@ pub enum ChainRequest { }, BuildConsensusState { - height: Height, + trusted: Height, + target: Height, + client_state: AnyClientState, reply_to: ReplyTo, }, @@ -222,8 +219,6 @@ pub trait ChainHandle: DynClone + Send + Sync + Debug { /// Send a transaction with `msgs` to chain. fn send_msgs(&self, proto_msgs: Vec) -> Result, Error>; - fn get_minimal_set(&self, from: Height, to: Height) -> Result, Error>; - fn get_signer(&self) -> Result; fn get_key(&self) -> Result; @@ -293,13 +288,19 @@ pub trait ChainHandle: DynClone + Send + Sync + Debug { &self, trusted_height: Height, target_height: Height, + client_state: AnyClientState, ) -> Result; /// Constructs a client state at the given height fn build_client_state(&self, height: Height) -> Result; /// Constructs a consensus state at the given height - fn build_consensus_state(&self, height: Height) -> Result; + fn build_consensus_state( + &self, + trusted: Height, + target: Height, + client_state: AnyClientState, + ) -> Result; fn build_connection_proofs_and_client_state( &self, diff --git a/relayer/src/chain/handle/prod.rs b/relayer/src/chain/handle/prod.rs index c9b047d0c3..881de4ff67 100644 --- a/relayer/src/chain/handle/prod.rs +++ b/relayer/src/chain/handle/prod.rs @@ -82,10 +82,6 @@ impl ChainHandle for ProdChainHandle { }) } - fn get_minimal_set(&self, from: Height, to: Height) -> Result, Error> { - self.send(|reply_to| ChainRequest::GetMinimalSet { from, to, reply_to }) - } - fn get_signer(&self) -> Result { self.send(|reply_to| ChainRequest::Signer { reply_to }) } @@ -214,10 +210,12 @@ impl ChainHandle for ProdChainHandle { &self, trusted_height: Height, target_height: Height, + client_state: AnyClientState, ) -> Result { self.send(|reply_to| ChainRequest::BuildHeader { trusted_height, target_height, + client_state, reply_to, }) } @@ -226,8 +224,18 @@ impl ChainHandle for ProdChainHandle { self.send(|reply_to| ChainRequest::BuildClientState { height, reply_to }) } - fn build_consensus_state(&self, height: Height) -> Result { - self.send(|reply_to| ChainRequest::BuildConsensusState { height, reply_to }) + fn build_consensus_state( + &self, + trusted: Height, + target: Height, + client_state: AnyClientState, + ) -> Result { + self.send(|reply_to| ChainRequest::BuildConsensusState { + trusted, + target, + client_state, + reply_to, + }) } fn build_connection_proofs_and_client_state( diff --git a/relayer/src/chain/mock.rs b/relayer/src/chain/mock.rs index 4986d8357f..ee9bd48d24 100644 --- a/relayer/src/chain/mock.rs +++ b/relayer/src/chain/mock.rs @@ -70,13 +70,8 @@ impl Chain for MockChain { }) } - #[allow(clippy::type_complexity)] - fn init_light_client( - &self, - ) -> Result<(Box>, Option>), Error> { - let light_client = MockLightClient::new(self); - - Ok((Box::new(light_client), None)) + fn init_light_client(&self) -> Result>, Error> { + Ok(Box::new(MockLightClient::new(self))) } fn init_event_monitor( diff --git a/relayer/src/chain/runtime.rs b/relayer/src/chain/runtime.rs index d83d78f474..fe221c0018 100644 --- a/relayer/src/chain/runtime.rs +++ b/relayer/src/chain/runtime.rs @@ -45,7 +45,6 @@ use ibc::ics02_client::client_consensus::AnyConsensusState; use ibc::ics02_client::client_state::AnyClientState; pub struct Threads { - pub light_client: Option>, pub chain_runtime: thread::JoinHandle<()>, pub event_monitor: Option>, } @@ -84,16 +83,15 @@ impl ChainRuntime { let chain = C::bootstrap(config, rt.clone())?; // Start the light client - let (light_client_handler, light_client_thread) = chain.init_light_client()?; + let light_client = chain.init_light_client()?; // Start the event monitor let (event_receiver, event_monitor_thread) = chain.init_event_monitor(rt.clone())?; // Instantiate & spawn the runtime - let (handle, runtime_thread) = Self::init(chain, light_client_handler, event_receiver, rt); + let (handle, runtime_thread) = Self::init(chain, light_client, event_receiver, rt); let threads = Threads { - light_client: light_client_thread, chain_runtime: runtime_thread, event_monitor: event_monitor_thread, }; @@ -173,10 +171,6 @@ impl ChainRuntime { self.send_msgs(proto_msgs, reply_to)? }, - Ok(ChainRequest::GetMinimalSet { from, to, reply_to }) => { - self.get_minimal_set(from, to, reply_to)? - } - Ok(ChainRequest::Signer { reply_to }) => { self.get_signer(reply_to)? } @@ -189,16 +183,16 @@ impl ChainRuntime { self.module_version(port_id, reply_to)? } - Ok(ChainRequest::BuildHeader { trusted_height, target_height, reply_to }) => { - self.build_header(trusted_height, target_height, reply_to)? + Ok(ChainRequest::BuildHeader { trusted_height, target_height, client_state, reply_to }) => { + self.build_header(trusted_height, target_height, client_state, reply_to)? } Ok(ChainRequest::BuildClientState { height, reply_to }) => { self.build_client_state(height, reply_to)? } - Ok(ChainRequest::BuildConsensusState { height, reply_to }) => { - self.build_consensus_state(height, reply_to)? + Ok(ChainRequest::BuildConsensusState { trusted, target, client_state, reply_to }) => { + self.build_consensus_state(trusted, target, client_state, reply_to)? } Ok(ChainRequest::BuildConnectionProofsAndClientState { message_type, connection_id, client_id, height, reply_to }) => { @@ -324,15 +318,6 @@ impl ChainRuntime { Ok(()) } - fn get_minimal_set( - &self, - _from: Height, - _to: Height, - _reply_to: ReplyTo>, - ) -> Result<(), Error> { - todo!() - } - fn get_signer(&mut self, reply_to: ReplyTo) -> Result<(), Error> { let result = self.chain.get_signer(); @@ -364,23 +349,25 @@ impl ChainRuntime { } fn build_header( - &self, + &mut self, trusted_height: Height, target_height: Height, + client_state: AnyClientState, reply_to: ReplyTo, ) -> Result<(), Error> { let header = { // Get the light block at trusted_height + 1 from chain. - // TODO - This is tendermint specific and needs to be refactored during - // the relayer light client refactoring. - // Note: This is needed to get the next validator set. While there is a next validator set - // in the light block at trusted height, the proposer is not known/set in this set. - let trusted_light_block = self - .light_client - .verify_to_target(trusted_height.increment())?; + // + // TODO: This is tendermint specific and needs to be refactored during + // the relayer light client refactoring. + // NOTE: This is needed to get the next validator set. While there is a next validator set + // in the light block at trusted height, the proposer is not known/set in this set. + let trusted_light_block = self.light_client.fetch(trusted_height.increment())?; // Get the light block at target_height from chain. - let target_light_block = self.light_client.verify_to_target(target_height)?; + let target_light_block = + self.light_client + .verify(trusted_height, target_height, &client_state)?; let header = self.chain @@ -416,15 +403,17 @@ impl ChainRuntime { /// Constructs a consensus state for the given height fn build_consensus_state( - &self, - height: Height, + &mut self, + trusted: Height, + target: Height, + client_state: AnyClientState, reply_to: ReplyTo, ) -> Result<(), Error> { - let latest_light_block = self.light_client.verify_to_target(height)?; + let light_block = self.light_client.verify(trusted, target, &client_state)?; let consensus_state = self .chain - .build_consensus_state(latest_light_block) + .build_consensus_state(light_block) .map(|cs| cs.wrap_any()); reply_to diff --git a/relayer/src/error.rs b/relayer/src/error.rs index d2d64b145d..680344c91b 100644 --- a/relayer/src/error.rs +++ b/relayer/src/error.rs @@ -3,7 +3,10 @@ use anomaly::{BoxError, Context}; use thiserror::Error; -use ibc::ics24_host::identifier::{ChainId, ChannelId, ConnectionId}; +use ibc::{ + ics02_client::client_type::ClientType, + ics24_host::identifier::{ChannelId, ConnectionId}, +}; /// An error that can be raised by the relayer. pub type Error = anomaly::Error; @@ -35,13 +38,9 @@ pub enum Kind { #[error("GRPC error")] Grpc, - /// Light client supervisor error - #[error("Light client supervisor error for chain id {0}")] - LightClientSupervisor(ChainId), - /// Light client instance error, typically raised by a `Client` - #[error("Light client instance error for rpc address {0}")] - LightClientInstance(String), + #[error("Light client error for RPC address {0}")] + LightClient(String), /// Trusted store error, raised by instances of `Store` #[error("Store error")] @@ -169,6 +168,12 @@ pub enum Kind { #[error("bech32 encoding failed")] Bech32Encoding(#[from] bech32::Error), + + #[error("client type mismatch: expected '{expected}', got '{got}'")] + ClientTypeMismatch { + expected: ClientType, + got: ClientType, + }, } impl Kind { diff --git a/relayer/src/foreign_client.rs b/relayer/src/foreign_client.rs index 83de334202..5b86202e96 100644 --- a/relayer/src/foreign_client.rs +++ b/relayer/src/foreign_client.rs @@ -241,7 +241,7 @@ impl ForeignClient { .wrap_any(); let consensus_state = self.src_chain - .build_consensus_state(latest_height) + .build_consensus_state(client_state.latest_height(), latest_height, client_state.clone()) .map_err(|e| ForeignClientError::ClientCreate(format!("failed while building client consensus state from src chain ({}) with error: {}", self.src_chain.id(), e)))? .wrap_any(); @@ -312,8 +312,8 @@ impl ForeignClient { thread::sleep(Duration::from_millis(100)) } - // Get the latest trusted height from the client state on destination. - let trusted_height = self + // Get the latest client state on destination. + let client_state = self .dst_chain() .query_client_state(&self.id, Height::default()) .map_err(|e| { @@ -321,8 +321,9 @@ impl ForeignClient { "failed querying client state on dst chain {} with error: {}", self.id, e )) - })? - .latest_height(); + })?; + + let trusted_height = client_state.latest_height(); if trusted_height >= target_height { warn!( @@ -334,7 +335,7 @@ impl ForeignClient { let header = self .src_chain() - .build_header(trusted_height, target_height) + .build_header(trusted_height, target_height, client_state) .map_err(|e| { ForeignClientError::ClientUpdate(format!( "failed building header with error: {}", diff --git a/relayer/src/light_client.rs b/relayer/src/light_client.rs index c9e3bc6d10..2191734899 100644 --- a/relayer/src/light_client.rs +++ b/relayer/src/light_client.rs @@ -1,3 +1,5 @@ +use ibc::ics02_client::client_state::AnyClientState; + use crate::chain::Chain; use crate::error; @@ -13,20 +15,14 @@ pub trait LightBlock: Send + Sync { /// Defines a client from the point of view of the relayer. pub trait LightClient: Send + Sync { - /// Get the latest trusted state of the light client - fn latest_trusted(&self) -> Result, error::Error>; - - /// Fetch and verify the latest header from the chain - fn verify_to_latest(&self) -> Result; - - /// Fetch and verify the header from the chain at the given height - fn verify_to_target(&self, height: ibc::Height) -> Result; + /// Fetch a header from the chain at the given height and verify it + fn verify( + &mut self, + trusted: ibc::Height, + target: ibc::Height, + client_state: &AnyClientState, + ) -> Result; - /// Compute the minimal ordered set of heights needed to update the light - /// client state from from `latest_client_state_height` to `target_height`. - fn get_minimal_set( - &self, - latest_client_state_height: ibc::Height, - target_height: ibc::Height, - ) -> Result, error::Error>; + /// Fetch a header from the chain at the given height, without verifying it + fn fetch(&mut self, height: ibc::Height) -> Result; } diff --git a/relayer/src/light_client/mock.rs b/relayer/src/light_client/mock.rs index ee4ca9c456..8ab73bc342 100644 --- a/relayer/src/light_client/mock.rs +++ b/relayer/src/light_client/mock.rs @@ -1,10 +1,14 @@ -use crate::chain::mock::MockChain; -use crate::chain::Chain; -use crate::error::Error; +use tendermint_testgen::light_block::TmLightBlock; + +use ibc::ics02_client::client_state::AnyClientState; use ibc::ics24_host::identifier::ChainId; use ibc::mock::host::HostBlock; use ibc::Height; +use crate::chain::mock::MockChain; +use crate::chain::Chain; +use crate::error::Error; + /// A light client serving a mock chain. pub struct LightClient { chain_id: ChainId, @@ -18,30 +22,22 @@ impl LightClient { } /// Returns a LightBlock at the requested height `h`. - fn light_block(&self, h: Height) -> ::LightBlock { + fn light_block(&self, h: Height) -> TmLightBlock { HostBlock::generate_tm_block(self.chain_id.clone(), h.revision_height) } } -#[allow(unused_variables)] impl super::LightClient for LightClient { - fn latest_trusted(&self) -> Result::LightBlock>, Error> { - unimplemented!() - } - - fn verify_to_latest(&self) -> Result<::LightBlock, Error> { - unimplemented!() + fn verify( + &mut self, + _trusted: Height, + target: Height, + _client_state: &AnyClientState, + ) -> Result { + Ok(self.light_block(target)) } - fn verify_to_target(&self, height: Height) -> Result<::LightBlock, Error> { + fn fetch(&mut self, height: Height) -> Result { Ok(self.light_block(height)) } - - fn get_minimal_set( - &self, - latest_client_state_height: Height, - target_height: Height, - ) -> Result, Error> { - unimplemented!() - } } diff --git a/relayer/src/light_client/tendermint.rs b/relayer/src/light_client/tendermint.rs index 5be75fb314..239032b776 100644 --- a/relayer/src/light_client/tendermint.rs +++ b/relayer/src/light_client/tendermint.rs @@ -1,149 +1,128 @@ use std::convert::TryFrom; +use tendermint_rpc as rpc; + use tendermint_light_client::{ - builder::LightClientBuilder, builder::SupervisorBuilder, light_client, store, supervisor, - supervisor::Handle, supervisor::Supervisor, types::Height as TMHeight, types::LightBlock, + components::{self, io::AtHeight}, + light_client::{LightClient as TmLightClient, Options as TmOptions}, + operations, + state::State as LightClientState, + store::{memory::MemoryStore, LightStore}, + types::Height as TMHeight, + types::{LightBlock, PeerId, Status}, }; -use tendermint_rpc as rpc; + +use ibc::{downcast, ics02_client::client_state::AnyClientState}; +use ibc::{ics02_client::client_type::ClientType, ics24_host::identifier::ChainId}; use crate::{ chain::CosmosSdkChain, - config::{ChainConfig, LightClientConfig, StoreConfig}, - error, + config::ChainConfig, + error::{self, Error}, }; -use ibc::ics24_host::identifier::ChainId; pub struct LightClient { - handle: Box, chain_id: ChainId, + peer_id: PeerId, + io: components::io::ProdIo, } impl super::LightClient for LightClient { - fn latest_trusted(&self) -> Result, error::Error> { - self.handle.latest_trusted().map_err(|e| { - error::Kind::LightClientSupervisor(self.chain_id.clone()) - .context(e) - .into() - }) - } + fn verify( + &mut self, + trusted: ibc::Height, + target: ibc::Height, + client_state: &AnyClientState, + ) -> Result { + let target_height = TMHeight::try_from(target.revision_height) + .map_err(|e| error::Kind::InvalidHeight.context(e))?; - fn verify_to_latest(&self) -> Result { - self.handle.verify_to_highest().map_err(|e| { - error::Kind::LightClientSupervisor(self.chain_id.clone()) - .context(e) - .into() - }) + let client = self.prepare_client(client_state)?; + let mut state = self.prepare_state(trusted)?; + + let light_block = client + .verify_to_target(target_height, &mut state) + .map_err(|e| error::Kind::LightClient(self.chain_id.to_string()).context(e))?; + + Ok(light_block) } - fn verify_to_target(&self, height: ibc::Height) -> Result { + fn fetch(&mut self, height: ibc::Height) -> Result { let height = TMHeight::try_from(height.revision_height) .map_err(|e| error::Kind::InvalidHeight.context(e))?; - self.handle.verify_to_target(height).map_err(|e| { - error::Kind::LightClientSupervisor(self.chain_id.clone()) - .context(e) - .into() - }) - } - - fn get_minimal_set( - &self, - _latest_client_state_height: ibc::Height, - _target_height: ibc::Height, - ) -> Result, error::Error> { - todo!() + self.fetch_light_block(AtHeight::At(height)) } } impl LightClient { - fn new(handle: impl Handle + 'static, chain_id: ChainId) -> Self { - Self { - handle: Box::new(handle), - chain_id, - } - } + pub fn from_config(config: &ChainConfig) -> Result { + let rpc_client = rpc::HttpClient::new(config.rpc_addr.clone()) + .map_err(|e| error::Kind::LightClient(config.rpc_addr.to_string()).context(e))?; + + let peer = config.primary().ok_or_else(|| { + error::Kind::LightClient(config.rpc_addr.to_string()).context("no primary peer") + })?; - pub fn from_config( - chain_config: &ChainConfig, - reset: bool, - ) -> Result<(Self, Supervisor), error::Error> { - let supervisor = build_supervisor(&chain_config, reset)?; - let handle = supervisor.handle(); + let io = components::io::ProdIo::new(peer.peer_id, rpc_client, Some(peer.timeout)); - Ok((Self::new(handle, chain_config.id.clone()), supervisor)) + Ok(Self { + chain_id: config.id.clone(), + peer_id: peer.peer_id, + io, + }) } -} -fn build_instance( - config: &LightClientConfig, - options: light_client::Options, - reset: bool, -) -> Result { - let rpc_client = rpc::HttpClient::new(config.address.clone()) - .map_err(|e| error::Kind::LightClientInstance(config.address.to_string()).context(e))?; - - let store: Box = match &config.store { - StoreConfig::Disk { path } => { - let db = sled::open(path).map_err(|e| { - error::Kind::LightClientInstance(config.address.to_string()).context(e) + fn prepare_client(&self, client_state: &AnyClientState) -> Result { + let clock = components::clock::SystemClock; + let hasher = operations::hasher::ProdHasher; + let verifier = components::verifier::ProdVerifier::default(); + let scheduler = components::scheduler::basic_bisecting_schedule; + + let client_state = + downcast!(client_state => AnyClientState::Tendermint).ok_or_else(|| { + error::Kind::ClientTypeMismatch { + expected: ClientType::Tendermint, + got: client_state.client_type(), + } })?; - Box::new(store::sled::SledStore::new(db)) - } - StoreConfig::Memory { .. } => Box::new(store::memory::MemoryStore::new()), - }; - - let builder = LightClientBuilder::prod( - config.peer_id, - rpc_client, - store, - options, - Some(config.timeout), - ); - - let builder = if reset { - builder.trust_primary_at(config.trusted_height, config.trusted_header_hash) - } else { - builder.trust_from_store() + + let params = TmOptions { + trust_threshold: client_state.trust_level, + trusting_period: client_state.trusting_period, + clock_drift: client_state.max_clock_drift, + }; + + Ok(TmLightClient::new( + self.peer_id, + params, + clock, + scheduler, + verifier, + hasher, + self.io.clone(), + )) } - .map_err(|e| error::Kind::LightClientInstance(config.address.to_string()).context(e))?; - Ok(builder.build()) -} + fn prepare_state(&self, trusted: ibc::Height) -> Result { + let trusted_height = TMHeight::try_from(trusted.revision_height) + .map_err(|e| error::Kind::InvalidHeight.context(e))?; + + let trusted_block = self.fetch_light_block(AtHeight::At(trusted_height))?; -fn build_supervisor(config: &ChainConfig, reset: bool) -> Result { - let options = light_client::Options { - trust_threshold: config.trust_threshold, - trusting_period: config.trusting_period, - clock_drift: config.clock_drift, - }; - - let primary_config = config.primary().ok_or_else(|| { - error::Kind::LightClientSupervisor(config.id.clone()) - .context("missing light client primary peer config") - })?; - - let witnesses_configs = config.witnesses().ok_or_else(|| { - error::Kind::LightClientSupervisor(config.id.clone()) - .context("missing light client witnesses peer config") - })?; - - let primary = build_instance(primary_config, options, reset)?; - - let mut witnesses = Vec::with_capacity(witnesses_configs.len()); - for conf in witnesses_configs { - let instance = build_instance(conf, options, reset)?; - witnesses.push((conf.peer_id, conf.address.clone(), instance)); + let mut store = MemoryStore::new(); + store.insert(trusted_block, Status::Trusted); + + Ok(LightClientState::new(store)) } - let supervisor = SupervisorBuilder::new() - .primary( - primary_config.peer_id, - primary_config.address.clone(), - primary, - ) - .witnesses(witnesses) - .map_err(|e| error::Kind::LightClientSupervisor(config.id.clone()).context(e))? - .build_prod(); - - Ok(supervisor) + fn fetch_light_block(&self, height: AtHeight) -> Result { + use tendermint_light_client::components::io::Io; + + self.io.fetch_light_block(height).map_err(|e| { + error::Kind::LightClient(self.chain_id.to_string()) + .context(e) + .into() + }) + } }