Plugin architecture and ledger integrations

I’m looking forward to seeing what you guys have come up with. In the meantime, I also want to share a sketch of the design we’ve been iterating on in the Rust implementation that may have some similarities.

High level points:

  • Every component is a Service that exposes the same API so they can be chained together into a connector, a standalone STREAM receiver, etc (credit to Carl Lerche’s work on the Rust Tower framework)
  • Services use deserialized ILP packets to minimize the number of times the packet is (de)serialized, and because most Services need to look at the data in the packets
  • Settlement messages do not have a separate sendMoney abstraction. They are either sent inside ILP packets or they are handled outside of the pipeline for ILP packets (for example, sent to a different HTTP URL path and handled directly by the settlement engine) (credit to @adrianhopebailie)
  • Each Service defines a trait (like an interface) to the data store with specific functions it needs (like get_routing_table, or atomically_update_balances). A data store could be in-memory, use a fast external system like Redis, or use a combination of database and pubsub technologies, and each store implementation would implement as many of the traits as possible.
  • There is a common AccountId that all Services and data stores use (instead of the plugins separately managing account details)

Service interface

The Service is a thing with two methods. poll_ready is used to determine whether the Service is ready to accept more requests. call takes a Request and asynchronously returns either an ILP Fulfill or Reject packet. A Request is an ILP Prepare packet with a from and to attached.

“Servers”, such as a BTP or HTTP server, accept an instance of a Service and call it with the deserialized Request. “Clients”, such as an outgoing HTTP client, implement this trait. “Middleware”, such as a balance updater, are passed a Service instance and implement the trait. Middleware pass on Requests by calling their inner Service and may modify the Request or the response, or respond directly without calling the inner Service.

pub type AccountId = u64;

pub struct Request {
    pub from: Option<AccountId>,
    pub to: Option<AccountId>,
    pub prepare: Prepare,
}

pub trait Service {
    type Future: Future<Item = Fulfill, Error = Reject> + Send + 'static;

    fn poll_ready(&mut self) -> Poll<(), ()> {
        Ok(Async::Ready(()))
    }

    fn call(&mut self, request: Request) -> Self::Future;
}

Note that @sentientwaffle and I are also looking into whether the Request's Option types can be replaced with traits so that the compiler can statically verify that Services are being chained together correctly (for example, a Service like an outgoing HTTP client should always be passed a to account.

Connector flow

Notes:

  • All of the Services are optional and they can be strung together in different configurations
  • The BTP Server and HTTP Server implement Service as a passthrough so they can be chained (credit to @sappenin)
  • The BTP Server and BTP Outgoing Services use the same pool of open sockets. Incoming connections to the Server are added to the pool, and the pool may be instantiated with URLs to connect to
  • I was previously working with a model that involved branching services such that the Router would decide which service to pass the Request to. This got complicated and @sappenin suggested thinking of the Connector as a single chain of Services, such that each Service would decide for itself whether to pass on the Request or handle it itself.
  • If settlement messages are sent in ILP packets, there could be Services that handle those and update the balances in the data store. Alternatively, all such packets could be forwarded to a separate settlement engine.
4 Likes