From f3b177918e0345fdb281e7dcd3d08b7bfae72e57 Mon Sep 17 00:00:00 2001 From: AbdelrahmanElawady Date: Thu, 21 Mar 2024 15:51:52 +0200 Subject: [PATCH] Listen to chain events and update twin on requests --- Cargo.lock | 1 + Cargo.toml | 3 ++- _tests/e2e_tests.rs | 3 +++ artifacts/network.scale | Bin 0 -> 83925 bytes proto/types.proto | 2 ++ src/bins/rmb-peer.rs | 11 ++++------- src/bins/rmb-relay.rs | 24 ++++++++++++++++-------- src/cache/redis.rs | 37 +++++++++++++++++++------------------ src/events/events.rs | 40 ++++++++++++++++++++++++++++++++++++++++ src/events/mod.rs | 3 +++ src/lib.rs | 2 ++ src/relay/api.rs | 40 +++++++++++++++++++++++++++++++++++++++- src/relay/mod.rs | 3 +++ src/tfchain/mod.rs | 6 ++++++ src/twin/mod.rs | 17 +++++++++++++++++ src/twin/substrate.rs | 5 +++++ src/types/mod.rs | 3 +++ 17 files changed, 165 insertions(+), 35 deletions(-) create mode 100644 artifacts/network.scale create mode 100644 src/events/events.rs create mode 100644 src/events/mod.rs create mode 100644 src/tfchain/mod.rs diff --git a/Cargo.lock b/Cargo.lock index f9116da..6e790fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3850,6 +3850,7 @@ dependencies = [ "mpart-async", "nix", "openssl", + "parity-scale-codec", "prometheus", "protobuf 3.3.0", "protobuf-codegen", diff --git a/Cargo.toml b/Cargo.toml index 41cb188..adf8287 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,8 @@ url = "2.3.1" tokio-tungstenite = { version = "0.20", features = ["native-tls"] } futures-util = "0.3.25" jwt = "0.16" -subxt = "0.28.0" +subxt = { version = "0.28.0", features = ["substrate-compat"]} +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "full", "bit-vec"] } itertools = "0.11" # for static build diff --git a/_tests/e2e_tests.rs b/_tests/e2e_tests.rs index 90eecf5..685aa0f 100644 --- a/_tests/e2e_tests.rs +++ b/_tests/e2e_tests.rs @@ -44,6 +44,9 @@ impl TwinDB for InMemoryDB { ) -> anyhow::Result> { unimplemented!() } + async fn set_twin(&self, twin: Twin) -> anyhow::Result<()> { + unimplemented!() + } } fn new_message( diff --git a/artifacts/network.scale b/artifacts/network.scale new file mode 100644 index 0000000000000000000000000000000000000000..d9891b778d01d6838efeae1f3c01a2ce92144421 GIT binary patch literal 83925 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 literal 0 HcmV?d00001 diff --git a/proto/types.proto b/proto/types.proto index 8e9cedb..e7ba3bf 100644 --- a/proto/types.proto +++ b/proto/types.proto @@ -71,4 +71,6 @@ message Envelope { bytes plain = 13; bytes cipher = 14; } + + optional string relays = 17; } diff --git a/src/bins/rmb-peer.rs b/src/bins/rmb-peer.rs index c62c4dc..8c39e06 100644 --- a/src/bins/rmb-peer.rs +++ b/src/bins/rmb-peer.rs @@ -1,6 +1,5 @@ use std::path::PathBuf; use std::str::FromStr; -use std::time::Duration; use anyhow::{Context, Result}; use clap::{builder::ArgAction, Args, Parser}; @@ -151,12 +150,10 @@ async fn app(args: Params) -> Result<()> { // cache is a little bit tricky because while it improves performance it // makes changes to twin data takes at least 5 min before they are detected - let db = SubstrateTwinDB::::new( - args.substrate, - RedisCache::new(pool.clone(), "twin", Duration::from_secs(60)), - ) - .await - .context("cannot create substrate twin db object")?; + let db = + SubstrateTwinDB::::new(args.substrate, RedisCache::new(pool.clone(), "twin")) + .await + .context("cannot create substrate twin db object")?; let id = db .get_twin_with_account(signer.account()) diff --git a/src/bins/rmb-relay.rs b/src/bins/rmb-relay.rs index 1732bed..a00230c 100644 --- a/src/bins/rmb-relay.rs +++ b/src/bins/rmb-relay.rs @@ -4,6 +4,7 @@ use std::time::Duration; use anyhow::{Context, Result}; use clap::{builder::ArgAction, Parser}; use rmb::cache::RedisCache; +use rmb::events; use rmb::redis; use rmb::relay::{ self, @@ -142,14 +143,13 @@ async fn app(args: Args) -> Result<()> { .await .context("failed to initialize redis pool")?; - // we use 6 hours cache for twin information because twin id will not change anyway - // and we only need twin public key for validation only. - let twins = SubstrateTwinDB::::new( - args.substrate, - RedisCache::new(pool.clone(), "twin", Duration::from_secs(args.cache * 60)), - ) - .await - .context("cannot create substrate twin db object")?; + let redis_cache = RedisCache::new(pool.clone(), "twin"); + + redis_cache.flush().await?; + + let twins = SubstrateTwinDB::::new(args.substrate.clone(), redis_cache.clone()) + .await + .context("cannot create substrate twin db object")?; let max_users = args.workers as usize * args.user_per_worker as usize; let opt = relay::SwitchOptions::new(pool.clone()) @@ -175,6 +175,14 @@ async fn app(args: Args) -> Result<()> { let r = relay::Relay::new(&args.domain, twins, opt, federation, limiter, ranker) .await .unwrap(); + + let l = events::Listener::new(args.substrate[0].as_str(), redis_cache).await?; + tokio::spawn(async move { + if let Err(e) = l.listen().await { + log::error!("failed to listen to events: {:#}", e); + } + }); + r.start(&args.listen).await.unwrap(); Ok(()) } diff --git a/src/cache/redis.rs b/src/cache/redis.rs index 13320f2..b303256 100644 --- a/src/cache/redis.rs +++ b/src/cache/redis.rs @@ -1,5 +1,3 @@ -use std::time::Duration; - use super::Cache; use anyhow::{Context, Result}; @@ -22,19 +20,13 @@ use serde::{de::DeserializeOwned, Serialize}; pub struct RedisCache { pool: Pool, prefix: String, - ttl: Duration, } impl RedisCache { - pub fn new>( - pool: Pool, - prefix: P, - ttl: Duration, - ) -> Self { + pub fn new>(pool: Pool, prefix: P) -> Self { Self { pool, prefix: prefix.into(), - ttl, } } @@ -47,6 +39,15 @@ impl RedisCache { Ok(conn) } + pub async fn flush(&self) -> Result<()> { + let mut conn = self.get_connection().await?; + cmd("DEL") + .arg(&self.prefix) + .query_async(&mut *conn) + .await?; + + Ok(()) + } } #[async_trait] @@ -57,12 +58,10 @@ where async fn set(&self, key: S, obj: T) -> Result<()> { let mut conn = self.get_connection().await?; let obj = serde_json::to_vec(&obj).context("unable to serialize twin object for redis")?; - let key = format!("{}.{}", self.prefix, key.to_string()); - cmd("SET") - .arg(key) + cmd("HSET") + .arg(&self.prefix) + .arg(key.to_string()) .arg(obj) - .arg("EX") - .arg(self.ttl.as_secs()) .query_async(&mut *conn) .await?; @@ -70,9 +69,12 @@ where } async fn get(&self, key: S) -> Result> { let mut conn = self.get_connection().await?; - let key = format!("{}.{}", self.prefix, key.to_string()); - let ret: Option> = cmd("GET").arg(key).query_async(&mut *conn).await?; + let ret: Option> = cmd("HGET") + .arg(&self.prefix) + .arg(key.to_string()) + .query_async(&mut *conn) + .await?; match ret { Some(val) => { @@ -93,7 +95,6 @@ mod tests { use super::*; const PREFIX: &str = "twin"; - const TTL: u64 = 20; async fn create_redis_cache() -> RedisCache { let manager = RedisConnectionManager::new("redis://127.0.0.1/") @@ -105,7 +106,7 @@ mod tests { .context("unable to build pool or redis connection manager") .unwrap(); - RedisCache::new(pool, PREFIX, Duration::from_secs(TTL)) + RedisCache::new(pool, PREFIX) } #[tokio::test] diff --git a/src/events/events.rs b/src/events/events.rs new file mode 100644 index 0000000..fb2c1f1 --- /dev/null +++ b/src/events/events.rs @@ -0,0 +1,40 @@ +use crate::{cache::Cache, tfchain::tfchain, twin::Twin}; +use anyhow::Result; +use futures::StreamExt; +use log; +use subxt::{OnlineClient, PolkadotConfig}; + +#[derive(Clone)] +pub struct Listener +where + C: Cache, +{ + cache: C, + api: OnlineClient, +} + +impl Listener +where + C: Cache + Clone, +{ + pub async fn new(url: &str, cache: C) -> Result { + let api = OnlineClient::::from_url(url).await?; + Ok(Listener { api, cache }) + } + pub async fn listen(&self) -> Result<()> { + log::info!("started chain events listener"); + let mut blocks_sub = self.api.blocks().subscribe_finalized().await?; + while let Some(block) = blocks_sub.next().await { + let events = block?.events().await?; + for evt in events.iter() { + let evt = evt?; + if let Ok(Some(twin)) = evt.as_event::() { + self.cache.set(twin.0.id, twin.0.into()).await?; + } else if let Ok(Some(twin)) = evt.as_event::() { + self.cache.set(twin.0.id, twin.0.into()).await?; + } + } + } + Ok(()) + } +} diff --git a/src/events/mod.rs b/src/events/mod.rs new file mode 100644 index 0000000..3a34494 --- /dev/null +++ b/src/events/mod.rs @@ -0,0 +1,3 @@ +mod events; + +pub use events::Listener; diff --git a/src/lib.rs b/src/lib.rs index 71e0a38..e3e063e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,10 +3,12 @@ extern crate anyhow; extern crate mime; pub mod cache; +pub mod events; pub mod identity; pub mod peer; pub mod redis; pub mod relay; +pub mod tfchain; pub mod token; pub mod twin; pub mod types; diff --git a/src/relay/api.rs b/src/relay/api.rs index 0c86fbd..c699bd6 100644 --- a/src/relay/api.rs +++ b/src/relay/api.rs @@ -1,5 +1,5 @@ use crate::token::{self, Claims}; -use crate::twin::TwinDB; +use crate::twin::{RelayDomains, TwinDB}; use crate::types::{Envelope, EnvelopeExt, Pong}; use anyhow::{Context, Result}; use futures::stream::SplitSink; @@ -17,6 +17,7 @@ use prometheus::TextEncoder; use protobuf::Message as ProtoMessage; use std::fmt::Display; use std::pin::Pin; +use std::str::FromStr; use std::sync::Arc; use tokio::sync::Mutex; @@ -203,6 +204,24 @@ async fn federation( let envelope = Envelope::parse_from_bytes(&body).map_err(|err| HttpError::BadRequest(err.to_string()))?; + + if let Some(relays) = &envelope.relays { + let mut twin = data + .twins + .get_twin(envelope.source.twin) + .await.map_err(|err| HttpError::FailedToGetTwin(err.to_string()))? + .ok_or_else(|| HttpError::TwinNotFound(envelope.source.twin))?; + let envelope_relays = match RelayDomains::from_str(relays) { + Ok(r) => r, + Err(_) => return Err(HttpError::BadRequest("invalid relays".to_string())), + }; + if let Some(twin_relays) = twin.relay { + if twin_relays != envelope_relays { + twin.relay = Some(envelope_relays); + data.twins.set_twin(twin).await.map_err(|err| HttpError::FailedToSetTwin(err.to_string()))?; + } + } + } let dst: StreamID = (&envelope.destination).into(); data.switch.send(&dst, &body).await?; @@ -309,6 +328,25 @@ impl Stream { .await? .ok_or_else(|| anyhow::Error::msg("unknown twin destination"))?; + if let Some(relays) = &envelope.relays { + let mut twin = self + .twins + .get_twin(envelope.source.twin) + .await? + .ok_or_else(|| anyhow::Error::msg("unknown twin source"))?; + + let envelope_relays = match RelayDomains::from_str(relays) { + Ok(r) => r, + Err(_) => anyhow::bail!("invalid relays"), + }; + if let Some(twin_relays) = twin.relay { + if twin_relays != envelope_relays { + twin.relay = Some(envelope_relays); + self.twins.set_twin(twin.clone()).await?; + } + } + } + if !twin .relay .ok_or_else(|| anyhow::Error::msg("relay info is not set for this twin"))? diff --git a/src/relay/mod.rs b/src/relay/mod.rs index df6532b..15db835 100644 --- a/src/relay/mod.rs +++ b/src/relay/mod.rs @@ -87,6 +87,8 @@ pub enum HttpError { InvalidJWT(#[from] token::Error), #[error("failed to get twin: {0}")] FailedToGetTwin(String), + #[error("failed to set twin: {0}")] + FailedToSetTwin(String), #[error("twin not found {0}")] TwinNotFound(u32), #[error("{0}")] @@ -109,6 +111,7 @@ impl HttpError { Self::MissingJWT => Codes::BAD_REQUEST, Self::InvalidJWT(_) => Codes::UNAUTHORIZED, Self::FailedToGetTwin(_) => Codes::INTERNAL_SERVER_ERROR, + Self::FailedToSetTwin(_) => Codes::INTERNAL_SERVER_ERROR, Self::TwinNotFound(_) => Codes::UNAUTHORIZED, Self::WebsocketError(_) => Codes::INTERNAL_SERVER_ERROR, Self::NotFound => Codes::NOT_FOUND, diff --git a/src/tfchain/mod.rs b/src/tfchain/mod.rs new file mode 100644 index 0000000..0832754 --- /dev/null +++ b/src/tfchain/mod.rs @@ -0,0 +1,6 @@ +#[subxt::subxt(runtime_metadata_path = "artifacts/network.scale")] +mod tfchain {} + +use subxt::utils::AccountId32; +pub use tfchain::runtime_types::pallet_tfgrid::types::Twin as TwinData; +pub type Twin = TwinData; diff --git a/src/twin/mod.rs b/src/twin/mod.rs index 17f2e63..a91edf1 100644 --- a/src/twin/mod.rs +++ b/src/twin/mod.rs @@ -15,10 +15,13 @@ use subxt::utils::AccountId32; pub trait TwinDB: Send + Sync + Clone + 'static { async fn get_twin(&self, twin_id: u32) -> Result>; async fn get_twin_with_account(&self, account_id: AccountId32) -> Result>; + async fn set_twin(&self, twin: Twin) -> Result<()>; } use tfchain_client::client::Twin as TwinData; +use crate::tfchain; + #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] pub struct Twin { pub id: u32, @@ -41,6 +44,20 @@ impl From for Twin { } } +impl From for Twin { + fn from(twin: tfchain::Twin) -> Self { + Twin { + id: twin.id, + account: twin.account_id, + relay: twin.relay.map(|v| { + let string: String = String::from_utf8_lossy(&v.0).into(); + RelayDomains::from_str(&string).unwrap_or_default() + }), + pk: twin.pk.map(|v| v.0), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, Eq)] pub struct RelayDomains(HashSet); diff --git a/src/twin/substrate.rs b/src/twin/substrate.rs index cf7b018..7ac9d21 100644 --- a/src/twin/substrate.rs +++ b/src/twin/substrate.rs @@ -78,6 +78,11 @@ where Ok(id) } + + async fn set_twin(&self, twin: Twin) -> Result<()> { + self.cache.set(twin.id, twin).await?; + Ok(()) + } } /// ClientWrapper is basically a substrate client. diff --git a/src/types/mod.rs b/src/types/mod.rs index ab26689..71dc005 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -338,6 +338,9 @@ impl Challengeable for Envelope { hash.write_all(data)?; } } + if let Some(ref relays) = self.relays { + write!(hash, "{}", relays)?; + } Ok(()) }