Scalable Messaging Model

There is a bunch in this post across a wide range of topics…sorry for the length…let me know if anything isn’t clear.

For some context, we’ve been working on a bunch of ‘batch by default’ and batch standards for Fungible and NFTs. Once these are out, I’d expect that the average message size may significantly increase as wallets, marketplaces, etc begin batching their requests. (An NFT market can now pull the whole list of NFTs for a collection and then make a request with all those IDs to a metadata endpoint and expect to get back, depending on the collection, a large response. So fewer requests, but bigger payloads.)

It is probably just a variable to stick into our calculus, but hopefully, we are developing some patterns that will extend to much more complicated standards than just Tokens.

Also, Question: Is the 1kb limit for both incoming payload and outgoing payload?

We have metadata variables that we give out on the token canisters like ICRC4:max_balance_batch that are there to specifically restrict users under the current 2MB limit. Will there be complications where the client doesn’t know what kind of response the server has implemented? Will the candid(did) expose it? What if one ICRC4 canister uses Small Message with Guaranteed response, but another uses Best effort and supports up to 2MB sized incoming batch…I guess phrased another way, how will canister clients know which method to use and/or are we going to have to go back and add stuff to existing ICRCs to handle this.

Oh man…this makes my head ache a bit trying to think of all the ways this could go sideways for folks that don’t know what they are doing, but I’d imagine some well-developed patterns would help here. We are already handling some deduplication on the ICRC canisters and I guess this pushes us to move to some kind of request-id generated client side(or deterministic key as is the case with ICRC1/2/4/7/37 transactions. It feels like getting back to a failed response might be a tough one. If the output was written to an ICRC3 log and dedup works, then hopefully you get a nice duplicate-of response. Still, you’re really going to need to make sure all relevant data is in that log in order to serialize it back to an expected object and inject it back into your processing pipeline.

Much of that feels like it leads to some ‘code smells’, but I guess you get to select this option intentionally. My concern is for the old pathway…will it get more expensive as we move forward if we don’t opt into these new modes?

I’d be interested in @dfx-json, @claudio, @luc-blaeser, +rest of motoko teams, though on how this would actually look in motoko.

    //calling
    let foo = await.with_size_limit(1024) myactor.transfer(...);

   //declaring
   public guranteed(msg) transfer(...) : async Bool{
   };

   or

   public shared(msg) transfer(...) : async.best_effort Bool{
   };

   or something else(we don't really have decorators yet).

We have been solving for these issues at the application level and have an alpha of a system that isn’t really ‘best effort’, but that assumes an event-based programming including archiving, replay, etc. It is specifically designed let canister ‘publish’ events to a trusted canister and not have to worry about any of the ‘untrusted’ stuff. The Broadcasters do everything via one-shots to subscribers and don’t wait for responses. If a subscriber is off-line it can catch up later by checking the nonce of the event stream.

The one thing it isn’t super good at for now is subnet awareness and/or optimizations, so it is possible to do something expensive like send an event to 100 subscribers on a different subnet instead of relaying to a broadcaster on the subnet and having it distribute to the 100 subscribers. I was hoping to get to that after the alpha.

It lays the foundations of some other cool features like cycle-neutral messaging, tokenization of data streams, etc. Given all of that and some grand designs that I may never actually have time to build…I would have actually loved something like this at the protocol layer.

Ethereum has events, but your contracts can’t respond to them. This event system fixes that glitch and lets you write programs in an event messaging style. When you do that you don’t have to stress about ‘what happens if I miss a message’ or ‘did the canister get it and do an update that I don’t have access to’ because you just assume that architecture from the beginning.

module {

   let handleEvent = EventHandlerHelper([
        ("icrc1.transfer", onTransfer),
        ("icrc2.transfer", onApprove),
        ...
   ]);

   var lastTransfer = 0;
   var lastApprove = 0;

    public shared(msg) handleEvent(eventId: Nat, publisherId: Principal, eventName: Text, payload: Candy.CandyValue){ //oneshot
    handleEvent(eventID, publisherID, eventName, payload);
};

   private func onTransfer(eventId: Nat, publisherId: Principal, eventName: Text, payload: Candy.CandyValue){
     if(eventID != lastEvent+1){ //this won't work if you use a filter at the event system level
        let missedEvents = eventSystem.catchUp(eventName, lastTransfer+1, eventID);
        for(thisItem in missedEvents){
          onTransfer(thisItem..);
        }
     };
     //transfer logic
 };

///etc

};

It certainly adds some state requirements and probably isn’t conducive to implementation in the replica, but I’d much rather be able to do the following then to have to write a bunch of retry code inline with the classic async/await approach:

   public event({caller, msg}) some_namespace_schema{
     //handle the event
   }; //and even better if the replica/motoko took care of making sure I didn't miss messages somehow.
5 Likes