Installation and usage of the platform

Example of a Docker smart contract with gRPC

This section describes an example of creating a simple Docker smart contract in Python. The smart contract uses gRPC interface to exchange data with a node.

Before you start, make sure that the utilities from the grpcio package for Python are installed on your machine:

pip3 install grpcio

To install and use the gRPC utilities for other available programming languages, see the official gRPC website.

Program description and listing

When a smart contract is initialized using the 103 transaction, the sum integer parameter with a value of 0 is set for it.

Whenever a smart contract is called using transaction 104, it returns an increment of the sum parameter (sum + 1).

Program listing:

import grpc
import os
import sys

from protobuf import common_pb2, contract_pb2, contract_pb2_grpc

CreateContractTransactionType = 103
CallContractTransactionType = 104

AUTH_METADATA_KEY = "authorization"

class ContractHandler:
    def __init__(self, stub, connection_id):
        self.client = stub
        self.connection_id = connection_id
        return

    def start(self, connection_token):
        self.__connect(connection_token)

    def __connect(self, connection_token):
        request = contract_pb2.ConnectionRequest(
            connection_id=self.connection_id
        )
        metadata = [(AUTH_METADATA_KEY, connection_token)]
        for contract_transaction_response in self.client.Connect(request=request, metadata=metadata):
            self.__process_connect_response(contract_transaction_response)

    def __process_connect_response(self, contract_transaction_response):
        print("receive: {}".format(contract_transaction_response))
        contract_transaction = contract_transaction_response.transaction
        if contract_transaction.type == CreateContractTransactionType:
            self.__handle_create_transaction(contract_transaction_response)
        elif contract_transaction.type == CallContractTransactionType:
            self.__handle_call_transaction(contract_transaction_response)
        else:
            print("Error: unknown transaction type '{}'".format(contract_transaction.type), file=sys.stderr)

    def __handle_create_transaction(self, contract_transaction_response):
        create_transaction = contract_transaction_response.transaction
        request = contract_pb2.ExecutionSuccessRequest(
            tx_id=create_transaction.id,
        r   esults=[common_pb2.DataEntry(
                    key="sum",
                    int_value=0)]
        )
        metadata = [(AUTH_METADATA_KEY, contract_transaction_response.auth_token)]
        response = self.client.CommitExecutionSuccess(request=request, metadata=metadata)
        print("in create tx response '{}'".format(response))

    def __handle_call_transaction(self, contract_transaction_response):
        call_transaction = contract_transaction_response.transaction
        metadata = [(AUTH_METADATA_KEY, contract_transaction_response.auth_token)]

        contract_key_request = contract_pb2.ContractKeyRequest(
            contract_id=call_transaction.contract_id,
            key="sum"
        )
        contract_key = self.client.GetContractKey(request=contract_key_request, metadata=metadata)
        old_value = contract_key.entry.int_value

        request = contract_pb2.ExecutionSuccessRequest(
            tx_id=call_transaction.id,
            results=[common_pb2.DataEntry(
                key="sum",
                int_value=old_value + 1)]
        )
        response = self.client.CommitExecutionSuccess(request=request, metadata=metadata)
        print("in call tx response '{}'".format(response))

def run(connection_id, node_host, node_port, connection_token):
    # NOTE(gRPC Python Team): .close() is possible on a channel and should be
    # used in circumstances in which the with statement does not fit the needs
    # of the code.
    with grpc.insecure_channel('{}:{}'.format(node_host, node_port)) as channel:
        stub = contract_pb2_grpc.ContractServiceStub(channel)
        handler = ContractHandler(stub, connection_id)
        handler.start(connection_token)

CONNECTION_ID_KEY = 'CONNECTION_ID'
CONNECTION_TOKEN_KEY = 'CONNECTION_TOKEN'
NODE_KEY = 'NODE'
NODE_PORT_KEY = 'NODE_PORT'

if __name__ == '__main__':
    if CONNECTION_ID_KEY not in os.environ:
        sys.exit("Connection id is not set")
    if CONNECTION_TOKEN_KEY not in os.environ:
        sys.exit("Connection token is not set")
    if NODE_KEY not in os.environ:
        sys.exit("Node host is not set")
    if NODE_PORT_KEY not in os.environ:
        sys.exit("Node port is not set")

    connection_id = os.environ['CONNECTION_ID']
    connection_token = os.environ['CONNECTION_TOKEN']
    node_host = os.environ['NODE']
    node_port = os.environ['NODE_PORT']

    run(connection_id, node_host, node_port, connection_token)

If you want transactions calling your contract to be able to be processed simultaneously, you must pass the async-factor parameter in the contract code itself. The contract passes the value of the async-factor parameter as part of the ConnectionRequest gRPC message defined in the contract_contract_service.proto file:

message ConnectionRequest {
string connection_id = 1;
int32 async_factor = 2;
}

Detailed information about parallel execution of smart contracts.

Authorization of a Docker smart contract with gRPC

To work with gRPC, a smart contract needs authorization. For the smart contract to work correctly with API methods, the following steps are performed:

  1. The following parameters must be defined in the environment variables of the smart contract:

  • CONNECTION_ID - connection identifier passed by the contract when connecting to a node;

  • CONNECTION_TOKEN - authorization token passed by the contract when connecting to a node;

  • NODE - IP address or domain name of the node;

  • NODE_PORT - port of the gRPC service deployed on the node.

The values of the NODE and NODE_PORT variables are taken from the node configuration file of the docker-engine.grpc-server section. The other variables are generated by the node and passed to the container when the smart contract is created.

Development of a Docker smart contract

1. In the directory that will contain your smart contract files, create an``src`` subdirectory and place the file contract.py with the smart contract code in it.

2. In the src directory, create a protobuf directory and put the following protobuf files in it:

  • contract_contract_service.proto

  • data_entry.proto

These files are placed in the we-proto-x.x.x.zip archive, which can be downloaded from the official GitHub repository of Waves Enterprise.

3. Generate the code of the gRPC methods in Python based on the contract_contract_service.proto file:

python3 -m grpc.tools.protoc -I. --python_out=. --grpc_python_out=. contract_contract_service.proto

As a result, two files will be created:

  • contract_contract_service_pb2.py

  • contract_contract_service_pb2_grpc.py

In the contract_contract_service_pb2.py file, change the line import data_entry_pb2 as data__entry__pb2 as follows:

import protobuf.data_entry_pb2 as data__entry__pb2

In the same way, change the line import contract_contract_service_pb2 as contract__contract__service__pb2 in the file contract_contract_service_pb2_grpc.py:

import protobuf.contract_contract_service_pb2 as contract__contract__service__pb2

Then generate an auxiliary file data_entry_pb2.py based on the data_entry.proto:

python3 -m grpc.tools.protoc -I. --python_out=. data_entry.proto

All three resulting files must be in the protobuf directory along with the source files.

4. Create a run.sh shell script, which will run the smart contract code in the container:

#!/bin/sh

eval $SET_ENV_CMD
python contract.py

Place the run.sh file in the root directory of your smart contract.

5. Create a Dockerfile script file to build and control the startup of your smart contract. When developing in Python, the basis for your smart contract image can be the official Python python:3.8-slim-buster'' image. Note that the packages ``dnsutils and grpcio-tools must be installed in the Docker container to make the smart contract work.

Dockerfile example:

FROM python:3.8-slim-buster
RUN apt update && apt install -yq dnsutils
RUN pip3 install grpcio-tools
ADD src/contract.py /
ADD src/protobuf/common_pb2.py /protobuf/
ADD src/protobuf/contract_pb2.py /protobuf/
ADD src/protobuf/contract_pb2_grpc.py /protobuf/
ADD run.sh /
RUN chmod +x run.sh
ENTRYPOINT ["/run.sh"]

Place the Dockerfile in the root directory of your smart contract.

6. In case you are working in the Waves Enterprise Mainnet, contact the Technical Support team to place your smart contract in the public repository.

If you work on a private network, build your smart contract yourself and place it in your own registry.

How a Docker smart contract with gRPC works

Once called, a Docker smart contract with gRPC works as follows:

  1. After the program starts, the presence of environment variables is checked.

  2. Using the values of the NODE and NODE_PORT environment variables, the contract creates a gRPC connection with a node.

  3. Next, the Connect stream method of the gRPC ContractService is called. The method receives a ConnectionRequest gRPC message, which specifies the connection identifier (obtained from the CONNECTION_ID environment variable). The method metadata contains the authorization header with the value of the authorization token (obtained from the CONNECTION_TOKEN environment variable).

  4. If the method is called successfully, a gRPC stream is returned with objects of type ContractTransactionResponse for execution. The object ContractTransactionResponse contains two fields:

    • transaction - a transaction to create or call a contract;

    • auth_token - authorization token specified in the authorization metadata header of the called method of gRPC services.

If transaction contains a 103 transaction, the initial state is initialized for the contract. If transaction contains a call transaction (the 104 transaction), the following actions are performed:

  • the value of sum key (GetContractKey method of the ContractService) is requested from the node;

  • the key value is incremented by one, i.e. sum = sum + 1;

  • The new key value is saved on the node (CommitExecutionSuccess method of the ContractService), i.e. the contract state is updated.

See also