Смарт-контракты Docker с использованием gRPC¶
Помимо использования REST API смарт-контракт может работать с нодой через фреймворк gRPC. Общее техническое описание особенностей реализации контрактов приведено в разделе Cмарт-контракты Docker. Перед написанием смарт-контракта необходимо выполнить подготовительные действия, приведенные в разделе Как использовать фреймворк gRPC
В следующем разделе рассмотрим пример создания смарт-контракта на Python, который выполняет операцию инкремента (увеличение заданного числа на единицу).
Описание работы смарт-контракта¶
В нашем примере транзакция 103 для создания контракта инициализирует начальное состояние контракта, сохраняя в нем числовой ключ sum
со значением 0
:
{
"key": "sum",
"type": "integer",
"value": 0
}
Каждая следующая транзакция вызова 104 увеличивает значение ключа sum
на единицу (т.е. sum = sum + 1
).
Как работает смарт-контракт после вызова:
После старта программы выполняется проверка на наличие переменных окружения. Переменные окружения, используемые контрактом:
CONNECTION_ID
– идентификатор соединения, передаваемый контрактом при соединении с нодой.CONNECTION_TOKEN
– токен авторизации, передаваемый контрактом при соединении с нодой.NODE
– ip-адрес или доменное имя ноды.NODE_PORT
– порт gRPC сервиса, развёрнутого на ноде.
Значения переменных
NODE
иNODE_PORT
берутся из конфигурационного файла ноды секции docker-engine.grpc-server. Остальные переменные генерируются нодой и передаются в контейнер при создании смарт контракта.Используя значения переменных окружения
NODE
иNODE_PORT
, контракт создает gRPC-подключение с нодой.Далее вызывается потоковый метод
Connect
gRPC сервисаContractService
(см. contract.proto файл). Метод принимает gRPC-сообщениеConnectionRequest
, в котором указывается идентификатор соединения (полученный из переменной окруженияCONNECTION_ID
). В метаданных метода указывается заголовокauthorization
со значением токена авторизации (полученного из переменной окруженияCONNECTION_TOKEN
).В случае успешного вызова метода возвращается gRPC поток (
stream
) с объектами типаContractTransactionResponse
для исполнения. ОбъектContractTransactionResponse
содержит два поля:transaction
– транзакция создания или вызова контракта.auth_token
– токен авторизации, указываемый в заголовкеauthorization
метаданных вызываемого метода gRPC сервисов.
Если
transaction
содержит транзакцию создания (тип транзакции – 103), то для контракта инициализируется начальное состояние. Еслиtransaction
содержит транзакцию вызова (тип транзакции – 104), то выполняются следующие действия:с ноды запрашивается значение ключа
sum
(методGetContractKey
сервисаContractService
);значение ключа увеличивается на единицу, т.е.
sum = sum + 1
);новое значение ключа сохраняется на ноде (метод
CommitExecutionSuccess
сервисаContractService
), т.е. происходит обновление состояния контракта.
Создание смарт-контракта¶
Скачайте и установите Docker for Developers (https://www.docker.com/get-started) для вашей операционной системы.
Подготовьте образ контракта. В папке с контрактом должны быть следующие файлы:
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
.
Если вы хотите, чтобы транзакции с вызовом вашего контракта могли обрабатываться одновременно, то необходимо в самом коде контракта передать параметр
async-factor
. Контракт передаёт значение параметраasync-factor
в составе gRPC-сообщенияConnectionRequest
:
message ConnectionRequest {
string connection_id = 1;
int32 async_factor = 2;
}
Значение параметра async-factor
может быть как заранее установленное в интервале от 1 до 999, так и динамически вычисляемое. Вы можете устанавливать фиксированное значение этого параметра как константу, однако рекомендуется устанавливать вычисляемое значение данного параметра. Например, контракт может запросить количество свободных ядер и передать это число в качестве значения параметра async-factor
. Это число будет использоваться для параллельной обработки транзакций с контрактом. Если параметр async-factor
не будет определён, то по умолчанию все транзакции с контрактом будут обрабатываться последовательно.
Обратите внимание, что не все средства разработки могут поддерживать параллельную обработку кода контракта. Также логика кода контракта должна учитывать специфику параллельного исполнения контракта. Подробнее о параллельной обработке контрактов можно почитать в разделе Параллельное исполнение контрактов.
Установите образ в 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
Для получения информации о смарт-контракте используйте команду
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
транзакциях с созданным смарт-контрактом.
Подпишите транзакцию 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
}
Отправьте подписанную транзакцию в блокчейн. Ответ от метода 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
) и убедитесь, что транзакция с инициализацией контракта размещена в блокчейне.
Вызов смарт-контракта¶
Подпишите транзакцию 104 на вызов смарт-контракта.
Пример запроса для транзакции вызова смарт-контракта:
{
"contractId": "2sqPS2VAKmK77FoNakw1VtDTCbDSa7nqh5wTXvJeYGo2",
"fee": 15000000,
"sender": "3PKyW5FSn4fmdrLcUnDMRHVyoDBxybRgP58",
"password": "",
"type": 104,
"version": 2,
"contractVersion": 1,
"params": []
}
Отправьте подписанную транзакцию в блокчейн. Ответ от метода sign необходимо передать на вход для метода broadcast.
Примечание
Параметры транзакции 104 (блоки, добавляемые в раздел params
) поддерживают 4 типа данных: string, integer, boolean, binary. Пример использования этих типов данных при оформлении вызова смарт-контракта приведен ниже.
Пример запроса на отправку транзакции вызова смарт-контракта в блокчейн:
{
"type": 104,
"id": "9fBrL2n5TN473g1gNfoZqaAqAsAJCuHRHYxZpLexL3VP",
"sender": "3PKyW5FSn4fmdrLcUnDMRHVyoDBxybRgP58",
"senderPublicKey": "2YvzcVLrqLCqouVrFZynjfotEuPNV9GrdauNpgdWXLsq",
"fee": 15000000,
"timestamp": 1549365736923,
"proofs": [
"2q4cTBhDkEDkFxr7iYaHPAv1dzaKo5rDaTxPF5VHryyYTXxTPvN9Wb3YrsDYixKiUPXBnAyXzEcnKPFRCW9xVp4v"
],
"version": 1,
"contractId": "2sqPS2VAKmK77FoNakw1VtDTCbDSa7nqh5wTXvJeYGo2",
"params": [ {
"type" : "string",
"value" : "data",
"key" : "action"
}, {
"type" : "integer",
"value" : 3,
"key" : "number"
}, {
"type" : "boolean",
"value" : true,
"key" : "isPositive"
}, {
"type" : "binary",
"value" : "base64:daaa",
"key" : "code"
} ]
}
Пример 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;
}
}