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
- Consider an application
Servicethat exposes a service that processes money transfers from anAccount.
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();
};
};
-
Consider an application
Clientthat wants to charge usage fees by usingService'sAccount.The client application needs a notification definition that corresponds to the service action that it wants to access. In this case the service is
Accountand the action istransfer, so the client needs to define a notification with the same signature astransfer.The client can then emit this notification and collect the response. In the example below,
MoneyTransferis the notification definition, and the notification is emitted whenevercollectFeeis 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;
}
};
};
};
-
Note that actual instances of both protocols must be created.
-
The owner of
Accountmust explicitly allow for transfers to occur. TheowneronAccountcan do this by creating a multi-nodeTokenContextthat binds a specificAccountprotocol 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
Serviceside changes (such as a party transfer or a migration), changes can be accounted for on theAccountend without theClient's involvement. -
On the
Clientside, the notificationMoneyTransfermust be bound to the multi-nodeTokenContextabove, along with the address of the node that is targeted and thereby being represented as aTokenBinding. TheTokenContextmust be obtained from theAccount's partyowner, which may or may not be the same party as the one on theClientend. 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. -
This interaction now works. Invoking
collectFeenow 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 theClientOperating Engine, which forwards it to the protocol as aNotifySuccessor aNotifyFailure.