Page: Follower contract service
2019-12-13 19:12
Follower contract service
Topic: Java API
See other Java API features at Software Developer Central: Java API at whole, Contracts Service, Distributed Storage, etc.
To start using Java API, you can add com.icodici:universa_core:3.13.3 dependency from Universa public Maven repository. See more details at Maven repository page.
This feature is available in package com.icodici.universa.node2.network
, as a part of Client
class; see it on GitHub at
Concepts
The main goal is tracking the contract registration events in any chain of contracts.
The “Follower contract” is one of several types of smart contracts that can be run on the node. Follower contract is the contract that tracks contract registrations in any chain of contracts in the network for payment, when a new registration event occurs, the contract sends a request to the URL specified by the user, which contains the body of the new registered revision.
The cost of servicing the Follower contract
The Follower contract works on the pre-paid basis.
When Follower contract is first created, it must be accompanied with a initial payment in the paying parcel, which should cover at least 100 origin-days (OD), or cost 100 callbacks. For example, in the simplest case, at the moment the minimum cost will be 100 U, which corresponds to the cost of 100 OD or 100 callbacks.
After the registration of the Follower contract, the payment will be spent on storage and on calling the callback.
When creating or updating the Follower contracts, the fee must not be lower than 100 U. The contract revision that does not match these limitations will not be approved by the nodes.
Any revision of Follower contract may contain additional fee with its paying parcel, to extend the registration time.
If the balance on the account is less than the cost of the callback, then the contract will be kept for some time. At this time you can replenish the account of the Follower contract. However, in this state, the contract will not cause a callback when registering a new revision of the tracked сontract.
Permissions to follow the contract
During the creation of a contract in the network of the universa, you can determine the necessary permissions for followers of your contract. By default, only the contract owner can be a follower of the contract.
However, for a contract you can allow:
- specify a limited list of followers;
- allow anyone to follow the contract.
In order to determine the necessary permissions for subscribers of your contract, you must add a field with a list of roles follower_roles
. Field follower_roles
can be defined in the definition
, state
or transactional
contract sections. The keys with which the Follower contract is signed must match at least one of the role roles from the list in the follower_roles
field.
Allow anyone to follow the contract, for example:
Contract.Definition cd = simpleContract.getDefinition();
istRole followerAllRole = new ListRole("all", 0, new ArrayList<>());
List<Role> followerAllRoles = Do.listOf(followerAllRole);
Binder data = new Binder();
data.set("follower_roles", followerAllRoles);
cd.setData(data);
The OD rate
OD can be purchased for U (stage 1) and UTN (stage 2) only. The current rate can be achieved with special API call.
API
Unless it is otherwise specified, all API calls are performed using Universa protected client channel using client key authorization.
Get the OD rate and cost callback
The Follower contract works on the pre-paid basis.
When Follower contract is first created, it must be accompanying with a initial payment in the paying parcel, which should cover at least 100 origin-days, or cost 100 callbacks.
In order to get the current price of the use of the follower contract, you need to use the command:
followerGetRate() -> { "rateOriginDays" : decimal_string, "rateCallback " : decimal_string }
Returns a Binder, containing the structure with the information, like:
rateOriginDays
is the amount of OD at the call moment, that can be paid for 1 UrateCallback
is callback cost in U at the call moment
There is no guarantee the rate will not be changed in even near future.
Create Follower contract
Method ContractsService.createFollowerContract creates and returns the ready-made FollowerContract contract with the specified permissions and values. FollowerContract is used to control and for payment for contract following operations.
FollowerContract createFollowerContract(Set
issuerKeys, Set ownerKeys, NSmartContract.NodeInfoProvider nodeInfoProvider)
issuerKeys
is issuer private keys;ownerKeys
is owner public keys;nodeInfoProvider
is node provider info.
Returns FollowerContract with need permissions and values.
Put tracking origin to Follower contract
Method FollowerContract.putTrackingOrigin puts new tracking origin
and his callback data (URL
and callback public key
) to the follower contract. If origin
is already present in follower contract, old callback data is replaced. If callback URL
is already present in follower contract, old callback key is replaced:
void putTrackingOrigin(HashId origin, String URL, PublicKey key)
origin
for tracking;URL
for callback if registered new revision with tracking origin;key
for checking receipt from callback by network.
Use the code:
FollowerContract followerContract = ContractsService.createFollowerContract(followerIssuerPrivateKeys, followerIssuerPublicKeys, nodeInfoProvider);
FollowerContract.putTrackingOrigin(simpleContract.getOrigin(), "http://localhost:7777/follow.callback", callbackKey.getPublicKey());
Querying the follower contract info
In order to request the information about the contract, use the command:
queryFollowerInfo(follower_id: binary) -> (follower state structure)
with param string address or binary hashId origin (only one of two is allowed).
Returns a Binder, which contains the follower state structure, like:
- int
paid_U
-> "100" - double
prepaid_OD
-> "100.0" - long
prepaid_from
-> "1539690798" - int
followed_origins
-> "1" - double
spent_OD
-> "0.0" - long
spent_OD_time
-> "1539690798" - double
callback_rate
-> "1.0" - Binder
callback_keys
-> dictionary with info about following origins - Binder
tracking_origins
-> dictionary with callback URLs and callback keys
Callback Server
When the user receives a callback from the network, he must respond to the request. The callback must be handled by the HTTP server launched on the user side at the URL specified in the Follower contract.
The callback-responding server must accept a callback from the Universa network and send a response to it and thereby confirm receipt of the callback from the network.
The answer is a receipt, the receipt contains the signature of the identifier of the new revision of the tracked contract on the key of the callback. The receipt is placed in the field receipt
of the server response.
An example of the user callback-accepting server code (also posted on GitHub at
/**
* Simple example of follower callback server. Use only to receive follower callbacks from Universa network.
* Follower callback server receives follower callback from Universa nodes, checks signature according to the key set of
* public keys of Universa nodes (optional), and sends a receipt to the node with a signed callback key.
*/
public class FollowerCallback {
private PrivateKey callbackKey;
private int port;
private String callbackURL;
private Set<PublicKey> nodeKeys;
protected BasicHTTPService service;
/**
* Initialize and start follower callback server.
*
* @param callbackKey is {@link PrivateKey} on which the follower callback server signs the response node
* @param port for listening by follower callback server
* @param callbackURL is URL to where callbacks are sent from node
*/
public FollowerCallback(PrivateKey callbackKey, int port, String callbackURL) throws IOException {
this.callbackKey = callbackKey;
this.port = port;
this.callbackURL = callbackURL;
service = new MicroHTTPDService();
addEndpoint(callbackURL, params -> onCallback(params));
service.start(port, 32);
System.out.println("Follower callback server started on port = " + port + " URL = " + callbackURL);
}
private void on(String path, BasicHTTPService.Handler handler) {
service.on(path, handler);
}
private void addEndpoint(String path, Endpoint ep) {
on(path, (request, response) -> {
Binder result;
try {
Result epResult = new Result();
ep.execute(extractParams(request), epResult);
result = epResult;
} catch (Exception e) {
result = new Binder();
}
response.setBody(Boss.pack(result));
});
}
void addEndpoint(String path, SimpleEndpoint sep) {
addEndpoint(path, (params, result) -> {
result.putAll(sep.execute(params));
});
}
private Binder extractParams(BasicHTTPService.Request request) {
Binder rp = request.getParams();
BasicHTTPService.FileUpload rd = (BasicHTTPService.FileUpload) rp.get("callbackData");
if (rd != null) {
byte[] data = rd.getBytes();
return Boss.unpack(data);
}
return Binder.EMPTY;
}
public interface Endpoint {
void execute(Binder params, Result result) throws Exception;
}
public interface SimpleEndpoint {
Binder execute(Binder params) throws Exception;
}
private Binder onCallback(Binder params) throws IOException {
byte[] packedItem = params.getBytesOrThrow("data").toArray();
Contract contract = Contract.fromPackedTransaction(packedItem);
System.out.println("Follower callback received. Contract: " + contract.getId().toString());
// check node key
if (nodeKeys != null) {
PublicKey nodeKey = new PublicKey(params.getBytesOrThrow("key").toArray());
byte[] signature = params.getBytesOrThrow("signature").toArray();
if (!nodeKey.verify(packedItem, signature, HashType.SHA512) || !nodeKeys.stream().anyMatch(n -> n.equals(nodeKey)))
return Binder.EMPTY;
}
// sign receipt
byte[] receipt = callbackKey.sign(contract.getId().getDigest(), HashType.SHA512);
System.out.println("Follower callback processed. Contract: " + contract.getId().toString());
return Binder.of("receipt", receipt);
}
/**
* Set public keys of Universa nodes for checking callback on follower callback server.
* If checking is successful, follower callback server sends a receipt to the node with a signed callback key.
*
* This checking is optional, and may be passed if Universa nodes keys not set or reset by
* {@link FollowerCallback#clearNetworkNodeKeys}.
*
* @param keys is set of {@link PublicKey} Universa nodes
*/
public void setNetworkNodeKeys(Set<PublicKey> keys) { nodeKeys = keys; }
/**
* Reset public keys of Universa nodes for disable checking callback on follower callback server.
*/
public void clearNetworkNodeKeys() { nodeKeys = null; }
/**
* Shutdown the follower callback server.
*/
public void shutdown() {
try {
service.close();
System.out.println("Follower callback server stopped on port = " + port + " URL = " + callbackURL);
} catch (Exception e) {}
}
class Result extends Binder {
private int status = 200;
public void setStatus(int code) {
status = code;
}
}
}
Callback Authentication
The onCallback
method creates a response to a callback from the Universa network.
If you want to check that the callback was sent from the Universa network, you can use the setNetworkNodeKeys
method to establish the public keys of the nodes.
If there is no need to authenticate the callback, then do not use the setNetworkNodeKeys
method, or clear the previously installed public keys using the clearNetworkNodeKeys
method.
In order to get a list of public keys of nodes, you need to use the getNodes()
method. The getNodes()
method returns a List<NodeRecord>
, the elements of which contain minimal information about the node, such as the URL and the public key.
An example of how to get the public keys of all the nodes and pass them to the setNetworkNodeKeys
method for checking the authenticity of the callback:
Set<PublicKey> nodeKeys = new HashSet<>();
List<Client.NodeRecord> nodes = client.getNodes();
nodes.forEach(node -> nodeKeys.add(node.key));
followerCallback.setNetworkNodeKeys(nodeKeys);