Docker-контракты через gRPC

Помимо использования REST API смарт-контракт может работать с нодой через фреймворк gRPC. gRPC — это высокопроизводительный фреймворк для вызов удаленных процедур (RPC), который работает поверх HTTP/2. В качестве инструмента описания типов данных и сериализации используется протокол Protobuf.

Подсказка

Техническое описание особенностей реализации контрактов приведено в разделе Смарт-контракты Docker.

Официально фреймворк gRPC поддерживает 10 языков программирования. Список языков вы можете найти в официальной документации gRPC. Рассмотрим пример создания смарт-контракта на Python, который выполняет операцию инкремента (увеличение заданного числа на единицу).

Описание работы смарт-контракта

В нашем примере транзакция 103 для создания контракта инициализирует начальное состояние контракта, сохраняя в нем числовой ключ sum со значением 0:

{
    "key": "sum",
    "type": "integer",
    "value": 0
}

Каждая следующая транзакция вызова 104 увеличивает значение ключа sum на единицу (т.е. sum = sum + 1).

Как работает смарт-контракт после вызова:

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

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

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

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

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

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

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

  3. Далее вызывается потоковый метод Connect gRPC сервиса ContractService (см. contract.proto файл). Метод принимает параметр 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), т.е. происходит обновление состояния контракта.

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

  1. Скачайте и установите Docker for Developers (https://www.docker.com/get-started) для вашей операционной системы.

  2. Подготовьте образ контракта. В папке с контрактом должны быть следующие файлы:

    • src/contract.py

    • Dockerfile

    • run.sh

    • src/protobuf/contract.proto

    • src/protobuf/common.proto

    • src/protobuf/common_pb2.py

    • src/protobuf/contract_pb2.py

    • src/protobuf/contract_pb2_grpc.py

    Файлы src/protobuf/common_pb2.py, src/protobuf/contract_pb2.py, src/protobuf/contract_pb2_grpc.py должны быть сгенерированы gRPC-компилятором из protobuf файлов contract.proto и common.proto. Описание процедуры генерации программных файлов из protobuf файлов вы моете найти на официальной странице gRPC.

    Важно

    После компиляции файлов необходимо поменять import директиву в сгенерированных файлах:

    • в файле contract_pb2.py должно быть import protobuf.common_pb2 as common__pb2;

    • в файле contract_pb2_grpc.py должно быть import protobuf.contract_pb2 as contract__pb2.

  3. Установите образ в Docker репозиторий образов. Если используете локальный репозиторий, выполните в терминале следующие команды:

docker run -d -p 5000:5000 --name registry registry:2
cd contracts/grpc-increment-contract
docker build -t grpc-increment-contract .
docker image tag grpc-increment-contract localhost:5000/grpc-increment-contract
docker start registry
docker push localhost:5000/grpc-increment-contract
  1. Для получения информации о смарт-контракте используйте команду docker inspect:

docker inspect 57c2c2d2643d
[
{
"Id": "sha256:57c2c2d2643da042ef8dd80010632ffdd11e3d2e3f85c20c31dce838073614dd",
"RepoTags": [
    "wenode:latest"
],
"RepoDigests": [],
"Parent": "sha256:d91d2307057bf3bb5bd9d364f16cd3d7eda3b58edf2686e1944bcc7133f07913",
"Comment": "",
"Created": "2019-10-25T14:15:03.856072509Z",
"Container": "",
"ContainerConfig": {
    "Hostname": "",
    "Domainname": "",
    "User": "",
    "AttachStdin": false,
    "AttachStdout": false,
    "AttachStderr": false,

Важно

Идентификатор Docker образа контракта Id является значением поля imageHash для использования в CreateContractTransaction транзакциях с созданным смарт-контрактом.

  1. Подпишите транзакцию 103 на создание смарт-контракта. В нашем примере транзакция подписывается ключом, сохраненным в keystore ноды. Описание REST API ноды и правила формирования транзакций приведены в разделе REST API.

Пример запроса для транзакции создания смарт-контракта:

{
    "fee": 100000000,
    "image": "localhost:5000/grpc-increment-contract",
    "imageHash": "7d3b915c82930dd79591aab040657338f64e5d8b842abe2d73d5c8f828584b65",
    "contractName": "grpc-increment-contract",
    "sender": "3PudkbvjV1nPj1TkuuRahh4sGdgfr4YAUV2",
    "password": "",
    "params": [],
    "type": 103,
    "version": 2,
}

Пример curl-запроса:

curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' --header 'X-Contract-Api-Token' -d '{ \
    "fee": 100000000, \
    "image": "localhost:5000/grpc-increment-contract", \
    "imageHash": "7d3b915c82930dd79591aab040657338f64e5d8b842abe2d73d5c8f828584b65", \
    "contractName": "grpc-increment-contract", \
    "sender": "3PudkbvjV1nPj1TkuuRahh4sGdgfr4YAUV2", \
    "password": "", \
    "params": [], \
    "type": 103, \
    "version": 2 \
}' 'http://localhost:6862/transactions/sign'

Пример ответа:

{
    "type": 103,
    "id": "ULcq9R7PvUB2yPMrmBdxoTi3bcRmQPT3JDLLLZVj4Ky",
    "sender": "3N3YTj1tNwn8XUJ8ptGKbPuEFNa9GFnhqew",
    "senderPublicKey": "3kW7vy6nPC59BXM67n5N56rhhAv38Dws5skqDsjMVT2M",
    "fee": 100000000,
    "timestamp": 1550591678479,
    "proofs": [ "yecRFZm9iBLyDy93bDVaNo1PR5Qkkic7196GAgUt9TNH1cnQphq4yGQQ8Fxj4BYA4TaqYVw5qxtWzGMPQyVeKYv" ],
    "version": 2,
    "image": "localhost:5000/grpc-increment-contract",
    "imageHash": "7d3b915c82930dd79591aab040657338f64e5d8b842abe2d73d5c8f828584b65",
    "contractName": "grpc-increment-contract",
    "params": [],
    "height": 1619
}
  1. Отправьте подписанную транзакцию в блокчейн. Ответ от метода sign необходимо передать на вход для метода broadcast.

Пример запроса на отправку транзакции создания смарт-контракта в блокчейн:

{
    "type": 103,
    "id": "ULcq9R7PvUB2yPMrmBdxoTi3bcRmQPT3JDLLLZVj4Ky",
    "sender": "3N3YTj1tNwn8XUJ8ptGKbPuEFNa9GFnhqew",
    "senderPublicKey": "3kW7vy6nPC59BXM67n5N56rhhAv38Dws5skqDsjMVT2M",
    "fee": 500000,
    "timestamp": 1550591678479,
    "proofs": [ "yecRFZm9iBLyDy93bDVaNo1PR5Qkkic7196GAgUt9TNH1cnQphq4yGQQ8Fxj4BYA4TaqYVw5qxtWzGMPQyVeKYv" ],
    "version": 1,
    "image": "stateful-increment-contract:latest",
    "imageHash": "7d3b915c82930dd79591aab040657338f64e5d8b842abe2d73d5c8f828584b65",
    "contractName": "stateful-increment-contract",
    "params": [],
    "height": 1619
}

Пример curl-запроса:

curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' --header 'X-Contract-Api-Token' -d '{ \
    "type": 103, \
    "id": "ULcq9R7PvUB2yPMrmBdxoTi3bcRmQPT3JDLLLZVj4Ky", \
    "sender": "3N3YTj1tNwn8XUJ8ptGKbPuEFNa9GFnhqew", \
    "senderPublicKey": "3kW7vy6nPC59BXM67n5N56rhhAv38Dws5skqDsjMVT2M", \
    "fee": 100000000, \
    "timestamp": 1550591678479, \
    "proofs": [ "yecRFZm9iBLyDy93bDVaNo1PR5Qkkic7196GAgUt9TNH1cnQphq4yGQQ8Fxj4BYA4TaqYVw5qxtWzGMPQyVeKYv" ], \
    "version": 2, \
    "image": "localhost:5000/grpc-increment-contract", \
    "imageHash": "7d3b915c82930dd79591aab040657338f64e5d8b842abe2d73d5c8f828584b65", \
    "contractName": "grpc-increment-contract", \
    "params": [], \
    "height": 1619 \
}' 'http://localhost:6862/transactions/broadcast'

Пример ответа:

{
    "type": 103,
    "id": "ULcq9R7PvUB2yPMrmBdxoTi3bcRmQPT3JDLLLZVj4Ky",
    "sender": "3N3YTj1tNwn8XUJ8ptGKbPuEFNa9GFnhqew",
    "senderPublicKey": "3kW7vy6nPC59BXM67n5N56rhhAv38Dws5skqDsjMVT2M",
    "fee": 100000000,
    "timestamp": 1550591678479,
    "proofs": [ "yecRFZm9iBLyDy93bDVaNo1PR5Qkkic7196GAgUt9TNH1cnQphq4yGQQ8Fxj4BYA4TaqYVw5qxtWzGMPQyVeKYv" ],
    "version": 2,
    "image": "localhost:5000/grpc-increment-contract",
    "imageHash": "7d3b915c82930dd79591aab040657338f64e5d8b842abe2d73d5c8f828584b65",
    "contractName": "grpc-increment-contract",
    "params": [],
    "height": 1619
}

Сравните идентификатор транзакции в обеих операциях (поле id) и убедитесь, что транзакция с инициализацией контракта размещена в блокчейне.

Вызов смарт-контракта

  1. Подпишите транзакцию 104 на вызов смарт-контракта.

Пример запроса для транзакции вызова смарт-контракта:

{
    "contractId": "2sqPS2VAKmK77FoNakw1VtDTCbDSa7nqh5wTXvJeYGo2",
    "fee": 15000000,
    "sender": "3PKyW5FSn4fmdrLcUnDMRHVyoDBxybRgP58",
    "password": "",
    "type": 104,
    "version": 2,
    "contractVersion": 1,
    "params": []
}
  1. Отправьте подписанную транзакцию в блокчейн. Ответ от метода sign необходимо передать на вход для метода broadcast.

Пример запроса на отправку транзакции вызова смарт-контракта в блокчейн:

{
    "type": 104,
    "id": "9fBrL2n5TN473g1gNfoZqaAqAsAJCuHRHYxZpLexL3VP",
    "sender": "3PKyW5FSn4fmdrLcUnDMRHVyoDBxybRgP58",
    "senderPublicKey": "2YvzcVLrqLCqouVrFZynjfotEuPNV9GrdauNpgdWXLsq",
    "fee": 15000000,
    "timestamp": 1549365736923,
    "proofs": [
        "2q4cTBhDkEDkFxr7iYaHPAv1dzaKo5rDaTxPF5VHryyYTXxTPvN9Wb3YrsDYixKiUPXBnAyXzEcnKPFRCW9xVp4v"
    ],
    "version": 1,
    "contractId": "2sqPS2VAKmK77FoNakw1VtDTCbDSa7nqh5wTXvJeYGo2",
    "params": []
}

Пример curl-запроса:

curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' --header 'X-Contract-Api-Token' -d '{ \
    "type": 104, \
    "id": "9fBrL2n5TN473g1gNfoZqaAqAsAJCuHRHYxZpLexL3VP", \
    "sender": "3PKyW5FSn4fmdrLcUnDMRHVyoDBxybRgP58", \
    "senderPublicKey": "2YvzcVLrqLCqouVrFZynjfotEuPNV9GrdauNpgdWXLsq", \
    "fee": 15000000, \
    "timestamp": 1549365736923, \
    "proofs": [ \
        "2q4cTBhDkEDkFxr7iYaHPAv1dzaKo5rDaTxPF5VHryyYTXxTPvN9Wb3YrsDYixKiUPXBnAyXzEcnKPFRCW9xVp4v" \
    ], \
    "version": 1, \
    "contractId": "2sqPS2VAKmK77FoNakw1VtDTCbDSa7nqh5wTXvJeYGo2", \
    "params": [] \
}' 'http://localhost:6862/transactions/broadcast'

Пример ответа:

[
    {
        "key": "sum",
        "type": "integer",
        "value": 2
    }
]

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

Примеры файлов

Листинг run.sh:

#!/bin/sh

eval $SET_ENV_CMD
python contract.py

Листинг 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"]

Листинг смарт-контракта на Python:

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)

Листинг contract.proto:

syntax = "proto3";
package wavesenterprise;

option java_multiple_files = true;
option java_package = "com.wavesplatform.protobuf.service";
option csharp_namespace = "WavesEnterprise";

import "google/protobuf/wrappers.proto";
import "common.proto";

service ContractService {

  rpc Connect (ConnectionRequest) returns (stream ContractTransactionResponse);

  rpc CommitExecutionSuccess (ExecutionSuccessRequest) returns (CommitExecutionResponse);

  rpc CommitExecutionError (ExecutionErrorRequest) returns (CommitExecutionResponse);

  rpc GetContractKeys (ContractKeysRequest) returns (ContractKeysResponse);

  rpc GetContractKey (ContractKeyRequest) returns (ContractKeyResponse);
}

message ConnectionRequest {
  string connection_id = 1;
}

message ContractTransactionResponse {
  ContractTransaction transaction = 1;
  string auth_token = 2;
}

message ContractTransaction {
  string id = 1;
  int32 type = 2;
  string sender = 3;
  string sender_public_key = 4;
  string contract_id = 5;
  repeated DataEntry params = 6;
  int64 fee = 7;
  int32 version = 8;
  bytes proofs = 9;
  int64 timestamp = 10;
  AssetId fee_asset_id = 11;

  oneof data {
    CreateContractTransactionData create_data = 20;
    CallContractTransactionData call_data = 21;
  }
}

message CreateContractTransactionData {
  string image = 1;
  string image_hash = 2;
  string contract_name = 3;
}

message CallContractTransactionData {
  int32 contract_version = 1;
}

message ExecutionSuccessRequest {
  string tx_id = 1;
  repeated DataEntry results = 2;
}

message ExecutionErrorRequest {
  string tx_id = 1;
  string message = 2;
}

message CommitExecutionResponse {
}

message ContractKeysRequest {
  string contract_id = 1;
  google.protobuf.Int32Value limit = 2;
  google.protobuf.Int32Value offset = 3;
  google.protobuf.StringValue matches = 4;
  KeysFilter keys_filter = 5;
}

message KeysFilter {
  repeated string keys = 1;
}

message ContractKeysResponse {
  repeated DataEntry entries = 1;
}

message ContractKeyRequest {
  string contract_id = 1;
  string key = 2;
}

message ContractKeyResponse {
  DataEntry entry = 1;
}

message AssetId {
  string value = 1;
}

Листинг common.proto:

syntax = "proto3";
package wavesenterprise;

option java_multiple_files = true;
option java_package = "com.wavesplatform.protobuf.common";
option csharp_namespace = "WavesEnterprise";

message DataEntry {
  string key = 1;
  oneof value {
    int64 int_value = 10;
    bool bool_value = 11;
    bytes binary_value = 12;
    string string_value = 13;
  }
}