Smart contract run with gRPC

In addition to using the REST API a smart contract can work with the node via the gRPC framework. gRPC is a high-performance remote procedure call (RPC) framework that runs over the HTTP/2 protocol. The protobuf protocol is used as a tool for describing of data types and serialization.

Hint

Technical description of contracts implementation is given in module Docker Smart Contracts.

gRPC framework supports 10 programming languages. You can find the list in official gRPC docs. We use an example of creating a Python smart contract that performs an increment operation (increasing a given number by one).

Description of the smart contract

In our example 103 transaction initializes the initial state of the contract for the creation, keeping the numeric key sum with 0 value in it:

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

Each next 104 call transaction increases the key value sum by one (sum = sum + 1).

How the smart contract works after the call:

  1. After the program runs, it checks for the presence of environment variables. There are environment variables which are used by the contract:

    • CONNECTION_ID – connection ID passed by the contract when connecting to a node.

    • CONNECTION_TOKEN – authorization token passed by the contract when connecting to a node.

    • NODE – a node IP address or a node domain name.

    • NODE_PORT – a gRPC port of the service which is deployed on the node.

    The values of the NODE and NODE_PORT variables are taken from :ref:`docker-engine.grpc-server <docker-configuration> section of the configuration file. Other variables are generated by the node and passed to the container when creating a smart contract.

  2. Using NODE and NODE_PORT variables values the contract creates gRPC connection to a node.

  3. Then gRPC ContractService service’s Connect method is called (see additional info in the contract.proto file). This method accepts ConnectionRequest gRPC message which is specifying the connection ID (CONNECTION_ID environment variable). Also in the methods metadata you need to specify the authorization head which contains an authorization token (CONNECTION_TOKEN environment variable).

  4. In the case of successful result gRPC stream is return including the ContractTransactionResponse objects for the execution. The ContractTransactionResponse object contains two fields:

    • transaction – a contract creation or call transaction.

    • auth_token – an authorization token, specified in the authorization head of metadata of gRPC method being called.

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

    • the node receives a request of the value of the sum key (the GetContractKey method of the ContractService service);

    • the key value increases by one, sum = sum + 1);

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

Smart contract creation

  1. Download and install Docker for Developers (https://www.docker.com/get-started) for your operating system.

  2. Prepare an image of the contract. The contract folder must contain the following files:

    • 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 files should be generated by the gRPC compiler using the contract.proto and common.proto protobuf files.

    Important

    After compiling the files you need to change the import directive in the generated files:

    • it must be import protobuf.common_pb2 as common__pb2 in the contract_pb2.py file;

    • it must be import protobuf.contract_pb2 as contract__pb2 in the contract_pb2_grpc.py file.

  3. If you want that your contracts transactions could be processed simultaneously, you should pass the async-factor parameter in the contract code itself. The contract passes the value of the async-factor parameter as part of the Connection Request gRPC message:

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

The value of the async-factor parameter can be pre-set in the range from 1 to 999, or dynamically calculated. You can set a fixed value for this parameter as a constant, but we recommend setting the calculated value for this parameter. For example, a contract can request the number of available cores and pass this number as the value of the async-factor parameter. This number will be used for parallel processing of contracts transactions. If the async-factor parameter is not defined, then all contracts transactions will be processed sequentially by default.

Note that not all development tools can support parallel processing of contract code. Also, the logic of the contract code should take into account the specifics of parallel execution of the contract. For more information about parallel contract processing, see Parallel contract execution.

  1. Install the image in the Docker image repository. If you are using a local repository, run the following commands in the terminal:

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. Use docker inspect command to get more info about smart contract:

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,

Important

The smart contract identifier Id is the value of the imageHash field and it is used in transactions with the created smart contract.

  1. Sign the 103 transaction for the smart contract creation. In our example the transaction is signed with a key stored in the node’s keystore. See REST API section for a description of the rest API nodes and rules for generating transactions.

Request sample of the contract creation transaction:

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

Curl-request sample:

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'

Response sample:

{
    "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. Send the signed transaction to the blockchain. A response from the sign method should be passed to broadcast method input.

Request sample for sending a smart contract creation transaction to the blockchain:

{
    "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-request sample:

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'

Response sample:

{
    "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
}

Compare transaction identifiers of both operations (id field) and make sure, that the initialization contract transaction has placed in the blockchain.

Smart contract call

  1. Sign the 104 transaction for the smart contract call.

Request sample of the contract call transaction:

{
    "contractId": "2sqPS2VAKmK77FoNakw1VtDTCbDSa7nqh5wTXvJeYGo2",
    "fee": 15000000,
    "sender": "3PKyW5FSn4fmdrLcUnDMRHVyoDBxybRgP58",
    "password": "",
    "type": 104,
    "version": 2,
    "contractVersion": 1,
    "params": []
}
  1. Send the signed transaction to the blockchain. A response from the sign method should be passed to broadcast method input.

Request sample for sending a smart contract call transaction to the blockchain:

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

Curl-request sample:

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'

Response sample:

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

Use the smart contract identifier to get info about an execution result.

Files samples

run.sh listing:

#!/bin/sh

eval $SET_ENV_CMD
python contract.py

Dockerfile listing:

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 smart contract 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)

contract.proto listing:

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 listing:

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;
  }
}