-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: move proxy logic into enclave #174
Conversation
I personally like the approach with On the |
Good points @dangush! After thinking about this for some time and looking at the current impl I feel like the better/easier short term impl would be to continue to support gRPC as the defacto quartz enclave protocol and write a separate WebSocket client/subscriber (non-tonic) service that calls into the relevant gRPC services based on the events that are relevant to them. The reason to support gRPC is so we can still support an external 'host' who can trigger enclave code so we don't depend entirely on WebSockets (this is useful for debugging and gives us more time to think about protocol and software design before we undertake a more substantial refactoring effort). Also, the core service still depends on the 'host' to initiate the handshake and doesn't listen to websockets, so that's another reason to keep gRPC support. So, this impl basically requires the following API ->
|
is this intern's salary $20/mo? 🤣 @dusterbloom |
@hu55a1n1 based on your last bullet point, am I right in understanding that you want the enclave to only be interfaced with via websocket, even for one-off requests to Core services? Were you thinking that this websocket listener would be ran in the enclave's
Why would this be specified by the gRPC service and not defined in the websocket logic? To clarify, when you say "every" here, that's because you're including core services too, right? So our websocket listener would have to differentiate between Core service requests and websocket data from the chain, and route various gRPC endpoints between core and application appropriately? It seems difficult to effectively define all the parameters under which a quartz dev may want to trigger their gRPC service. Wouldn't it make more sense to decouple the gRPC logic with whatever triggers it? The MTCS and transfers quartz apps both use the pattern of watching for an event but doesn't necessarily have to hold for all quartz apps. This convo seems like one frustrating debate over flexibility vs simplicity haha. Regardless, in the case of doing this, what are the parameters that we want to define for a gRPC service? Thinking through writing:
let mut subs = client
.subscribe(Query::from(EventType::Tx).and_contains("wasm.action", "init_clearing"))
|
No, actually I think there should be two interfaces but the API should be such that specifying one should be sufficient (i.e. the websocket calls can be inferred from the gRPC ones - will try to add some example code to demonstrate this but it's more of a DevX improvement)
I think we can use use tonic::transport::Server;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize your gRPC service
let my_service = MyGrpcService::default();
// Clone the service if you need to share it across tasks
let cloned_service = my_service.clone();
// Start the WebSocket listener in a separate task
tokio::spawn(async move {
websocket_listener(cloned_service, "ws://example.com/socket").await;
});
// Start the gRPC server
Server::builder()
.add_service(MyGrpcServiceServer::new(my_service))
.serve("127.0.0.1:50051".parse()?)
.await?;
Ok(())
}
Good point! I should have worded by comment differently. What I meant was that ideally users only had to write one (gRPC) service, and then implement a trait or something to specify what must be done when a relevant websocket event was received. All this with good DevX and little boilerplate.
Interesting! I think I was working under the assumption that every app will want to react based on an event. Maybe that's not true. I propose we work on an ADR to discuss this further. Or maybe we should do more sync brainstorming meetings. |
This is sort of what I had in mind -> #[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
/* ... */
QuartzServer::new(config.clone(), sk.clone(), attestor.clone())
.add_service(TransfersServer::new(TransfersService::new(
config, sk, attestor,
)))
.serve(args.rpc_addr)
.await?;
Ok(())
}
pub struct QuartzServer {
pub router: Router,
}
impl QuartzServer {
pub fn new(
config: Config,
sk: Arc<Mutex<Option<SigningKey>>>,
attestor: DefaultAttestor,
) -> Self {
Self {
router: Server::builder().add_service(CoreServer::new(CoreService::new(
config,
sk.clone(),
attestor.clone(),
))),
}
}
pub fn add_service<S>(mut self, service: S) -> Self
where
S: Service<Request<BoxBody>, Response = Response<BoxBody>, Error = Infallible>
+ NamedService
+ Clone
+ Send
+ 'static,
S::Future: Send + 'static,
{
self.router = self.router.add_service(service);
// add route to ws listener
self
}
pub async fn serve(self, addr: SocketAddr) -> Result<(), Error> {
// spawn ws listener as a tokio task
self.router.serve(addr).await
}
}
pub enum TmAbciEvent { /* ... */ }
pub struct WebsocketListener {
handlers: Vec<Box<dyn WebSocketHandler<Event=TmAbciEvent>>>
} And require all services to implement pub trait WebSocketHandler {
type Event;
fn handle_event(&self, event: Self::Event) -> Result<(), Error>;
} And that's how these services define how they want their relevant websocket events to be handled and how they'd call methods on themselves. |
here's how I've implemented it: #[async_trait::async_trait]
pub trait WebSocketListener: Send + 'static {
async fn listen(&self) -> Result<(), Error>;
}
pub struct QuartzServer {
pub router: Router,
ws_listeners: Vec<Box<dyn WebSocketListener>>,
}
impl QuartzServer {
pub fn new(
config: Config,
sk: Arc<Mutex<Option<SigningKey>>>,
attestor: DefaultAttestor,
) -> Self {
let core_service = CoreServer::new(CoreService::new(config, sk.clone(), attestor.clone()));
Self {
router: Server::builder().add_service(core_service),
ws_listeners: Vec::new(),
}
}
pub fn add_service<S>(mut self, service: S) -> Self
where
S: Service<
http::request::Request<BoxBody>,
Response = http::response::Response<BoxBody>,
Error = Infallible,
>
+ WebSocketListener
+ Send
+ Clone
+ 'static
+ NamedService,
S::Future: Send + 'static,
{
self.router = self.router.add_service(service.clone());
self.ws_listeners.push(Box::new(service));
self
}
} I'd like to avoid cloning the service struct if possible. To do this I want to box just the future returned by the listener function as opposed to the whole service. pub struct QuartzServer {
pub router: Router,
ws_listeners: Vec<Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>>,
}
pub fn add_service<S>(mut self, service: S) -> Self
where
S: Service<
http::request::Request<BoxBody>,
Response = http::response::Response<BoxBody>,
Error = Infallible,
>
+ WebSocketListener
+ Send
+ 'static
+ NamedService,
S::Future: Send + 'static,
{
self.router = self.router.add_service(service.clone());
self.ws_listeners.push(Box::pin(async move {
WebSocketListener::listen(&service).await
}));
self
} This clone defeats the purpose of boxing only the future and not the entire service. So I think I arrived at the point where you said
I tried doing some stuff to make the QuartzServer add_service function take an Is this just a useless pursuit? |
@dangush I was testing this to see if I could already upgrade to wasmd v.0.52. The following happened:
$ MOCK_SGX=1 && rm -rf ../apps/transfers/.cache && cargo run -- --mock-sgx --app-dir "../apps/transfers/" dev --unsafe-trust-latest --contract-manifest "../apps/transfers/contracts/Cargo.toml" --init-msg '{"denom":"ucosm"}'
wasmd query wasm contract-state smart $CONTRACT '{"get_balance":{"address":"wasm1mkrm9m8g0dzv5z73xg8yzlj6srqc72qru5xfv3"}}'
data: "" Upon more digging, I found this error in the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @dangush! Great stuff!👌 Just did a code review, going to test now.
|
||
// Send setoffs to mtcs contract on chain | ||
let output = | ||
wasmd_client.tx_execute(contract, chain_id, 2000000, &sender, json!(setoffs_msg))?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any specific reason why we're using the sender
of the init_clearing
msg here and not the CLI arg tx_sender
? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also the gas is hardcoded which probably shouldn't be the case? (There are also concerns about non-determinism (retry, etc.) that this notion of sending txs from an enclave introduces - will open a separate issue to discuss them)
I'm seeing this error when I run
|
c4bd111
to
66ebbb2
Compare
d554d2b
to
8138f9b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
Summary
Move the
listen.sh
/listen.rs
script logic into an enclave service.Implementation
Per @hu55a1n1, this is done by creating a new service which will subscribe to a WS stream upon being requested to via
grpcurl
The resulting UX is that it requires two processes / shells, just like how it was with the listen script. One needs to run the enclave which serves the tonic server, and one that makes a request to the Listen endpoint which runs the websocket subscriber and makes
Run
requests. IMO, this is undesirable UX even if having two processes is unavoidable, so we should add whatever tooling and syntactic sugar we can around it.Since fully "running" the enclave logic now requires a way of making a grpcurl request, I'm thinking that we should provide a cleaner tool for this in the CLI. This command would basically do what running the listen script did before.
quartz enclave run --service "" --endpoint "" --request ""
Thoughts:
The 'service' argument is bloaty and IMO breaks abstractions. But if we want the user to design their quartz app enclaves with multiple tonic services chained together (like how the listen service is rn) then something like this would be necessary
Otherwise, it could be simplified to take no arguments and only provide access to a standardized, required
listen
endpoint defined in the additional service (which can be required from the quartz dev like Quartz Core). However, this makes the quartz framework more opinionated / less flexible (devs may not wantlisten
functionality at all)I don't think I can use the tonic client to dynamically call endpoints so this would have to be implemented with something like
grpcurl
under the hood. Idk I guess makes no real difference, just something I noticedI'd appreciate feedback on the matter of the cli tool. Will implement after dev is merged