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:
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
andNODE_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.Using
NODE
andNODE_PORT
variables values the contract creates gRPC connection to a node.Then gRPC
ContractService
service’sConnect
method is called (see additional info in the contract.proto file). This method acceptsConnectionRequest
gRPC message which is specifying the connection ID (CONNECTION_ID
environment variable). Also in the methods metadata you need to specify theauthorization
head which contains an authorization token (CONNECTION_TOKEN
environment variable).In the case of successful result gRPC
stream
is return including theContractTransactionResponse
objects for the execution. TheContractTransactionResponse
object contains two fields:transaction
– a contract creation or call transaction.auth_token
– an authorization token, specified in theauthorization
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. Iftransaction
contains a call transaction (transaction type – 104), the following actions are performed:the node receives a request of the value of the
sum
key (theGetContractKey
method of theContractService
service);the key value increases by one,
sum = sum + 1
);a new key value is saved on the node (the
CommitExecutionSuccess
method of theContractService
service), i.e. the contract state is updated.
Smart contract creation¶
Download and install Docker for Developers (https://www.docker.com/get-started) for your operating system.
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 thecontract.proto
andcommon.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 thecontract_pb2.py
file;it must be
import protobuf.contract_pb2 as contract__pb2
in thecontract_pb2_grpc.py
file.
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 theasync-factor
parameter as part of theConnection 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.
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
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.
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
}
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¶
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": []
}
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;
}
}