Установка и использование платформы

Пример смарт-контракта с использованием gRPC

В этом разделе рассмотрен пример создания простого смарт-контракта на Python. Для обмена данными с нодой смарт-контракт применяет gRPC-интерфейс.

Перед началом работы убедитесь, что на вашей машине установлены утилиты из пакета grpcio для Python:

pip3 install grpcio

Порядок установки и использования gRPC-утилит для других доступных языков программирования приведен на официальном сайте gRPC.

Описание и листинг программы

При инициализации смарт контракта при помощи транзакции 103, для него устанавливается целочисленный параметр sum со значением 0.

При вызове каждом вызове смарт-контракта при помощи транзакции 104, он возвращает инкремент параметра sum (sum + 1).

Листинг программы:

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)

Если вы хотите, чтобы транзакции с вызовом вашего контракта могли обрабатываться одновременно, то необходимо в самом коде контракта передать параметр async-factor. Контракт передаёт значение параметра async-factor в составе gRPC-сообщения ConnectionRequest, определенном в файле contract_contract_service.proto:

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

Подробнее о параллельном исполнении смарт-контрактов.

Авторизация смарт-контракта c gRPC

Для работы с gRPC смарт-контракту необходима авторизация. Чтобы смарт-контракт корректно работал с методами API, выполняются следующие действия:

  1. В переменных окружения смарт-контракта должны быть определены следующие параметры:

  • CONNECTION_ID – идентификатор соединения, передаваемый контрактом при соединении с нодой;

  • CONNECTION_TOKEN – токен авторизации, передаваемый контрактом при соединении с нодой;

  • NODE – ip-адрес или доменное имя ноды;

  • NODE_PORT – порт gRPC сервиса, развёрнутого на ноде.

Значения переменных NODE и NODE_PORT берутся из конфигурационного файла ноды секции docker-engine.grpc-server. Остальные переменные генерируются нодой и передаются в контейнер при создании смарт контракта.

Создание смарт-контракта

1. В директории, которая будет содержать файлы вашего смарт-контракта, создайте поддиректорию src и поместите в нее файл contract.py с кодом смарт-контракта.

2. В директории src создайте директорию protobuf и поместите в нее следующие protobuf-файлы:

  • contract_contract_service.proto

  • data_entry.proto

Эти файлы помещены в архив we-proto-x.x.x.zip, размещенном в официальном GitHub-репозитории Waves Enterprise.

3. Сгенерируйте код gRPC-методов на Python на основе файла contract_contract_service.proto:

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

В результате будет создано два файла:

  • contract_contract_service_pb2.py

  • contract_contract_service_pb2_grpc.py

В файле contract_contract_service_pb2.py измените строку import data_entry_pb2 as data__entry__pb2 следующим образом:

import protobuf.data_entry_pb2 as data__entry__pb2

Таким же образом измените строку import contract_contract_service_pb2 as contract__contract__service__pb2 в файле contract_contract_service_pb2_grpc.py:

import protobuf.contract_contract_service_pb2 as contract__contract__service__pb2

Затем сгенерируйте вспомогательный файл data_entry_pb2.py на основе data_entry.proto:

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

Все три полученных файла должны находиться в директории protobuf вместе с исходными файлами.

4. Создайте shell-скрипт run.sh, который будет запускать код смарт-контракта в контейнере:

#!/bin/sh

eval $SET_ENV_CMD
python contract.py

Поместите файл run.sh в корневую директорию вашего смарт-контракта.

5. Создайте сценарный файл Dockerfile для сборки и управления запуском смарт-контракта. При разработке на Python основой образа вашего смарт-контракта может служить официальный образ Python python:3.8-slim-buster. Обратите внимание, что для обеспечения работы смарт-контракта в контейнере Docker должны быть установлены пакеты dnsutils и grpcio-tools.

Пример Dockerfile:

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"]

Поместите Dockerfile в корневую директорию вашего смарт-контракта.

6. Свяжитесь со службой технической поддержки Waves Enterprise для помещения вашего смарт-контракта в открытый репозиторий, если вы работаете в Waves Enterprise Mainnet.

Если вы работаете в частной сети, соберите смарт-контракт самостоятельно и разместите его в собственном репозитории.

Как работает смарт-контракт с использованием gRPC

После вызова смарт-контракт с gRPC работает следующим образом:

  1. После старта программы выполняется проверка на наличие переменных окружения.

  2. Используя значения переменных окружения NODE и NODE_PORT, контракт создает gRPC-подключение с нодой.

  3. Далее вызывается потоковый метод Connect gRPC-сервиса ContractService. Метод принимает gRPC-сообщение ConnectionRequest, в котором указывается идентификатор соединения (полученный из переменной окружения CONNECTION_ID). В метаданных метода указывается заголовок authorization со значением токена авторизации (полученного из переменной окружения CONNECTION_TOKEN).

  4. В случае успешного вызова метода возвращается gRPC-поток (stream) с объектами типа ContractTransactionResponse для исполнения. Объект ContractTransactionResponse содержит два поля:

    • transaction – транзакция создания или вызова контракта;

    • auth_token – токен авторизации, указываемый в заголовке authorization метаданных вызываемого метода gRPC сервисов.

Если transaction содержит транзакцию 103), то для контракта инициализируется начальное состояние. Если transaction содержит транзакцию вызова (тип транзакции – 104), то выполняются следующие действия:

  • с ноды запрашивается значение ключа sum (метод GetContractKey сервиса ContractService);

  • значение ключа увеличивается на единицу, т.е. sum = sum + 1);

  • новое значение ключа сохраняется на ноде (метод CommitExecutionSuccess сервиса ContractService), т.е. происходит обновление состояния контракта.

Смотрите также