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.8.4 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.

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 U
  • rateCallback 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's server code (also posted on the Github):


/**
 * 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);