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
Service
that 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
Client
that 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 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;
}
};
};
};
-
Note that actual instances of both protocols must be created.
-
The owner of
Account
must explicitly allow for transfers to occur. Theowner
onAccount
can do this by creating a multi-nodeTokenContext
that binds a specificAccount
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.
-
On the
Client
side, the notificationMoneyTransfer
must be bound to the multi-nodeTokenContext
above, along with the address of the node that is targeted and thereby being represented as aTokenBinding
. TheTokenContext
must be obtained from theAccount
's partyowner
, which may or may not be the same party as the one on theClient
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. -
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 theClient
Operating Engine, which forwards it to the protocol as aNotifySuccess
or aNotifyFailure
.