Skip to content

Multi-node

Introduction

Protocols in different applications can communicate with one another natively via node-to-node messaging, even if the applications are not on the same Operating Engine.

Node-to-node messaging has been designed such that nodes do not need to share any infrastructure, and new nodes may be interacted with without additional setup or having to share authentication services.

While the simple direct-messaging approach is currently the only natively supported option, the design supports other implementations of message buses depending on the business requirements. These include:

  • Proper message queues such as ActiveMQ or Kafka hosted by the service node, which allow for message handling transparency (e.g., fairness when submitting orders).
  • DLT-based messaging, which allows for non-repudiation.

Example

  1. Consider an application Service that exposes a service that processes money transfers from an Account.
struct DebitEntry { toAccount: Text, amount: Number };

@api
protocol[owner] Account(private var balance: Number) {
    private var debit = listOf<DebitEntry>();

    permission[owner] transfer(toAccount: Text, amount: Number) returns Text {
        require(balance >= amount, "Balance cannot be negative.");

        balance = balance - amount;
        debit = debit.with(DebitEntry(toAccount, amount));

        return this.toText() + "-" + (debit.size() - 1).toText();
    };
};
  1. Consider an application Client that wants to charge usage fees by using Service's Account.

The client application needs a notification definition that corresponds to the service action that it wants to access. In this case the service is Account and the action is transfer, so the client needs to define a notification with the same signature as transfer.

The client can then emit this notification and collect the response. In the example below, MoneyTransfer is the notification definition, and the notification is emitted whenever collectFee is invoked.

See Notify for details on NPL notifications.

@multinode
notification MoneyTransfer(toAccount: Text, amount: Number) returns Text;

@api
protocol[owner] Client() {
    initial state unpaid;
    state failed;
    final state paid;

    private var transactionIds = listOf<Text>();

    @api
    permission[owner] collectFee(toAccount: Text) | unpaid, failed {
        notify MoneyTransfer(toAccount, 10) resume collectResponse;
    };

    permission[owner] collectResponse(r: NotifyResult<Text>) {
        match (r) {
            is NotifySuccess<Text> -> {
                transactionIds = transactionIds.with(r.result);
                become paid;
            }

            is NotifyFailure -> {
                become failed;
            }
        };
    };
};
  1. Note that actual instances of both protocols must be created.

  2. The owner of Account must explicitly allow for transfers to occur. The owner on Account can do this by creating a multi-node TokenContext that binds a specific Account protocol instance, the corresponding action, and the party. These tokens are therefore context-specific and may be revoked by the issuer at any time.

If this setup on the Service side changes (such as a party transfer or a migration), changes can be accounted for on the Account end without the Client's involvement.

  1. On the Client side, the notification MoneyTransfer must be bound to the multi-node TokenContext above, along with the address of the node that is targeted and thereby being represented as a TokenBinding. The TokenContext must be obtained from the Account's party owner, which may or may not be the same party as the one on the Client end. The token exchange mechanism is not within the scope of the Operating Engine itself, but there is room for automation especially if both Operating Engines use the same identity/access management service.

  2. This interaction now works. Invoking collectFee now triggers a notification that is picked up by the operating engine, which forwards a message to the target node. The target node verifies the token and, if valid, produces a response. The response is picked up by the Client Operating Engine, which forwards it to the protocol as a NotifySuccess or a NotifyFailure.