use std::{
fmt::{Debug, Display},
marker::PhantomData,
net::SocketAddr,
path::PathBuf,
};
use anyhow::anyhow;
use cosmrs::AccountId;
use futures_util::StreamExt;
use log::{error, info, trace, warn};
use quartz_proto::quartz::core_server::{Core, CoreServer};
use reqwest::Url;
use serde::Serialize;
use tendermint_rpc::{
event::Event as TmEvent,
query::{EventType, Query},
SubscriptionClient, WebSocketClient,
};
use tokio::sync::mpsc::Receiver;
use tonic::{transport::Server, Status};
use tonic_health::server::health_reporter;
use crate::{
backup_restore::Backup,
chain_client::{
default::{DefaultChainClient, DefaultTxConfig},
ChainClient,
},
event::QuartzEvent,
handler::Handler,
store::Store,
Enclave, Notification,
};
pub type Response<R, E> = <R as Handler<E>>::Response;
#[async_trait::async_trait]
pub trait Host: Send + Sync + 'static + Sized {
type ChainClient: ChainClient;
type Enclave: Enclave;
type Error: Send + Sync + 'static;
type Event: Handler<Self::ChainClient, Response = Self::Request>;
type Request: Handler<Self::Enclave>;
async fn enclave_call(
&self,
request: Self::Request,
) -> Result<Response<Self::Request, Self::Enclave>, Self::Error>;
async fn serve_with_query(
self,
url: Url,
rpc_addr: SocketAddr,
query: Option<Query>,
) -> Result<(), Self::Error>;
async fn serve(self, url: Url, rpc_addr: SocketAddr) -> Result<(), Self::Error> {
self.serve_with_query(url, rpc_addr, None).await
}
}
#[derive(Debug)]
pub struct DefaultHost<R, EV, GF, E, C = DefaultChainClient> {
enclave: E,
chain_client: C,
gas_fn: GF,
backup_path: Option<PathBuf>,
notifier_rx: Receiver<Notification>,
_phantom: PhantomData<(R, EV)>,
}
impl<R, EV, GF, E, C> DefaultHost<R, EV, GF, E, C>
where
R: Handler<E>,
C: ChainClient,
{
pub fn new(
enclave: E,
chain_client: C,
gas_fn: GF,
backup_path: Option<PathBuf>,
notifier_rx: Receiver<Notification>,
) -> Self {
Self {
enclave,
chain_client,
gas_fn,
backup_path,
notifier_rx,
_phantom: Default::default(),
}
}
}
#[async_trait::async_trait]
impl<R, EV, GF, E, C> Host for DefaultHost<R, EV, GF, E, C>
where
E: Enclave + Backup<Config = PathBuf, Error = anyhow::Error> + Clone + Core,
<E as Enclave>::Store: Store<Contract = AccountId>,
C: ChainClient<Contract = AccountId, Error = anyhow::Error>,
<C as ChainClient>::TxOutput: Display,
R: Handler<E, Error = Status> + Debug,
<R as Handler<E>>::Response: Iterator + Send + Sync,
<<R as Handler<E>>::Response as Iterator>::Item: Serialize + Send + Sync + 'static,
EV: Handler<C, Response = R, Error = anyhow::Error>,
EV: TryFrom<TmEvent, Error = anyhow::Error>,
GF: GasProvider<<R as Handler<E>>::Response, C> + Send + Sync + 'static,
{
type ChainClient = C;
type Enclave = E;
type Error = anyhow::Error;
type Event = QuartzEvent<EV>;
type Request = R;
async fn enclave_call(
&self,
request: Self::Request,
) -> Result<Response<Self::Request, Self::Enclave>, Self::Error> {
request
.handle(&self.enclave)
.await
.map_err(|e| anyhow!("enclave call failed: {}", e))
}
async fn serve_with_query(
mut self,
url: Url,
rpc_addr: SocketAddr,
query: Option<Query>,
) -> Result<(), Self::Error> {
let (health_reporter, health_service) = health_reporter();
health_reporter.set_not_serving::<CoreServer<E>>().await;
let enclave = self.enclave.clone();
tokio::spawn(async move {
Server::builder()
.add_service(health_service)
.add_service(CoreServer::new(enclave))
.serve(rpc_addr)
.await
});
if let Some(ref backup_path) = self.backup_path {
if self.enclave.has_backup(backup_path.clone()).await {
info!("found backup; attempting to restore after 30s...");
busy_wait_iters(3_000_000_000);
let restore_res = self.enclave.try_restore(backup_path.clone()).await;
if let Err(e) = restore_res {
error!("failed to restore from backup: {e}");
}
} else {
info!("no backup found; waiting for handshake completion...");
}
} else {
info!("backup path not specified; skipping backup/restore operations");
}
if let Some(Notification::HandshakeComplete) = self.notifier_rx.recv().await {
if let Some(ref backup_path) = self.backup_path {
self.enclave.backup(backup_path.clone()).await?;
}
}
let (client, driver) = WebSocketClient::new(url.as_str()).await.unwrap();
let driver_handle = tokio::spawn(async move { driver.run().await });
let query = query.unwrap_or(Query::from(EventType::Tx));
let mut subs = client.subscribe(query).await.unwrap();
info!("enclave ready...");
health_reporter.set_serving::<CoreServer<E>>().await;
while let Some(Ok(event)) = subs.next().await {
trace!("Received event: {event:?}");
let event = match Self::Event::try_from(event) {
Ok(e) => e,
Err(e) => {
trace!("Failed to decode event: {e}");
continue;
}
};
let contract = event.contract.clone();
let expected_contract = self
.enclave
.store()
.await
.get_contract()
.await
.map_err(|_| anyhow!("contract read failure"))?
.expect("contract must be set");
if contract != expected_contract {
error!("contract != expected_contract");
continue;
}
let request = match event.handle(&self.chain_client).await {
Ok(r) => r,
Err(e) => {
warn!("event handler: {e}");
continue;
}
};
trace!("Handling request: {request:?}");
let response = match self.enclave_call(request).await {
Ok(r) => r,
Err(e) => {
error!("request handler: {e}");
continue;
}
};
let gas_info = self
.gas_fn
.gas_for_tx(&response, &self.chain_client, &contract)
.await?;
let output = self
.chain_client
.send_tx(&contract, response, gas_info)
.await;
match output {
Ok(o) => info!("tx output: {o}"),
Err(e) => warn!("send_tx: {e}"),
}
}
client.close().expect("Failed to close client");
let _ = driver_handle.await;
Ok(())
}
}
#[inline(never)]
fn busy_wait_iters(mut iters: u64) {
use core::sync::atomic::{AtomicU64, Ordering};
static SPIN_TICK: AtomicU64 = AtomicU64::new(0);
while iters != 0 {
std::hint::black_box(SPIN_TICK.fetch_add(1, Ordering::Relaxed));
core::hint::spin_loop();
iters -= 1;
}
}
#[async_trait::async_trait]
pub trait GasProvider<Tx, CC> {
async fn gas_for_tx(
&self,
tx: &Tx,
chain_client: &CC,
contract: &AccountId,
) -> Result<DefaultTxConfig, anyhow::Error>;
}