I am trying to build a custom transaction processor for sawtooth but I have run into a wall and since then have been stuck there. I created a sawtooth test network using docker on my local machine (using this guide). To test my custom transaction processor I modified the docker compose file to publish ports of the validator-0 and rest-api-0 to the host machine. I have tried accessing the rest api from the browser and it works fine. However the problem occurs when I try to run my custom transaction processor. The logs on the validator terminal shows a message as follows:
sawtooth-validator-default-0 | [2022-03-23 13:17:21.492 INFO dispatch] received a message of type TP_REGISTER_REQUEST from d9de3f928215c306cf719fba501bae475c209cb41cad6efd55b410d7918dfe657f0f9211580c8b3249baec3aaf87d0fc84b58aee1c1501b8c65a7c8fc1ad14f8 but have no handler for that type
The modified docker compose file is below
# Copyright 2019 Cargill Incorporated
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
version: '3.6'
volumes:
pbft-shared:
services:
# -------------=== rest api ===-------------
rest-api-0:
image: hyperledger/sawtooth-rest-api:nightly
container_name: sawtooth-rest-api-default-0
# expose:
# - 8008
ports:
- 8008:8008
command: |
bash -c "
sawtooth-rest-api \
--connect tcp://validator-0:4004 \
--bind rest-api-0:8008
"
stop_signal: SIGKILL
rest-api-1:
image: hyperledger/sawtooth-rest-api:nightly
container_name: sawtooth-rest-api-default-1
expose:
- 8008
command: |
bash -c "
sawtooth-rest-api \
--connect tcp://validator-1:4004 \
--bind rest-api-1:8008
"
stop_signal: SIGKILL
rest-api-2:
image: hyperledger/sawtooth-rest-api:nightly
container_name: sawtooth-rest-api-default-2
expose:
- 8008
command: |
bash -c "
sawtooth-rest-api \
--connect tcp://validator-2:4004 \
--bind rest-api-2:8008
"
stop_signal: SIGKILL
rest-api-3:
image: hyperledger/sawtooth-rest-api:nightly
container_name: sawtooth-rest-api-default-3
expose:
- 8008
command: |
bash -c "
sawtooth-rest-api \
--connect tcp://validator-3:4004 \
--bind rest-api-3:8008
"
stop_signal: SIGKILL
rest-api-4:
image: hyperledger/sawtooth-rest-api:nightly
container_name: sawtooth-rest-api-default-4
expose:
- 8008
command: |
bash -c "
sawtooth-rest-api \
--connect tcp://validator-4:4004 \
--bind rest-api-4:8008
"
stop_signal: SIGKILL
# -------------=== shell ===-------------
shell:
image: hyperledger/sawtooth-shell:nightly
container_name: sawtooth-shell-default
volumes:
- pbft-shared:/pbft-shared
command: |
bash -c "
sawtooth keygen
tail -f /dev/null
"
stop_signal: SIGKILL
# -------------=== validators ===-------------
validator-0:
image: hyperledger/sawtooth-validator:nightly
container_name: sawtooth-validator-default-0
# expose:
# - 4004
# - 5050
# - 8800
ports:
- "4004:4004"
- "5050:5050"
- "8800:8800"
volumes:
- pbft-shared:/pbft-shared
command: |
bash -c "
if [ -e /pbft-shared/validators/validator-0.priv ]; then
cp /pbft-shared/validators/validator-0.pub /etc/sawtooth/keys/validator.pub
cp /pbft-shared/validators/validator-0.priv /etc/sawtooth/keys/validator.priv
fi &&
if [ ! -e /etc/sawtooth/keys/validator.priv ]; then
sawadm keygen
mkdir -p /pbft-shared/validators || true
cp /etc/sawtooth/keys/validator.pub /pbft-shared/validators/validator-0.pub
cp /etc/sawtooth/keys/validator.priv /pbft-shared/validators/validator-0.priv
fi &&
if [ ! -e config-genesis.batch ]; then
sawset genesis -k /etc/sawtooth/keys/validator.priv -o config-genesis.batch
fi &&
while [[ ! -f /pbft-shared/validators/validator-1.pub || \
! -f /pbft-shared/validators/validator-2.pub || \
! -f /pbft-shared/validators/validator-3.pub || \
! -f /pbft-shared/validators/validator-4.pub ]];
do sleep 1; done
echo sawtooth.consensus.pbft.members=\\['\"'$$(cat /pbft-shared/validators/validator-0.pub)'\"','\"'$$(cat /pbft-shared/validators/validator-1.pub)'\"','\"'$$(cat /pbft-shared/validators/validator-2.pub)'\"','\"'$$(cat /pbft-shared/validators/validator-3.pub)'\"','\"'$$(cat /pbft-shared/validators/validator-4.pub)'\"'\\] &&
if [ ! -e config.batch ]; then
sawset proposal create \
-k /etc/sawtooth/keys/validator.priv \
sawtooth.consensus.algorithm.name=pbft \
sawtooth.consensus.algorithm.version=1.0 \
sawtooth.consensus.pbft.members=\\['\"'$$(cat /pbft-shared/validators/validator-0.pub)'\"','\"'$$(cat /pbft-shared/validators/validator-1.pub)'\"','\"'$$(cat /pbft-shared/validators/validator-2.pub)'\"','\"'$$(cat /pbft-shared/validators/validator-3.pub)'\"','\"'$$(cat /pbft-shared/validators/validator-4.pub)'\"'\\] \
sawtooth.publisher.max_batches_per_block=1200 \
-o config.batch
fi &&
if [ ! -e /var/lib/sawtooth/genesis.batch ]; then
sawadm genesis config-genesis.batch config.batch
fi &&
if [ ! -e /root/.sawtooth/keys/my_key.priv ]; then
sawtooth keygen my_key
fi &&
sawtooth-validator -vv \
--endpoint tcp://validator-0:8800 \
--bind component:tcp://eth0:4004 \
--bind consensus:tcp://eth0:5050 \
--bind network:tcp://eth0:8800 \
--scheduler parallel \
--peering static \
--maximum-peer-connectivity 10000
"
validator-1:
image: hyperledger/sawtooth-validator:nightly
container_name: sawtooth-validator-default-1
expose:
- 4004
- 5050
- 8800
volumes:
- pbft-shared:/pbft-shared
command: |
bash -c "
if [ -e /pbft-shared/validators/validator-1.priv ]; then
cp /pbft-shared/validators/validator-1.pub /etc/sawtooth/keys/validator.pub
cp /pbft-shared/validators/validator-1.priv /etc/sawtooth/keys/validator.priv
fi &&
if [ ! -e /etc/sawtooth/keys/validator.priv ]; then
sawadm keygen
mkdir -p /pbft-shared/validators || true
cp /etc/sawtooth/keys/validator.pub /pbft-shared/validators/validator-1.pub
cp /etc/sawtooth/keys/validator.priv /pbft-shared/validators/validator-1.priv
fi &&
sawtooth keygen my_key &&
sawtooth-validator -vv \
--endpoint tcp://validator-1:8800 \
--bind component:tcp://eth0:4004 \
--bind consensus:tcp://eth0:5050 \
--bind network:tcp://eth0:8800 \
--scheduler parallel \
--peering static \
--maximum-peer-connectivity 10000 \
--peers tcp://validator-0:8800
"
validator-2:
image: hyperledger/sawtooth-validator:nightly
container_name: sawtooth-validator-default-2
expose:
- 4004
- 5050
- 8800
volumes:
- pbft-shared:/pbft-shared
command: |
bash -c "
if [ -e /pbft-shared/validators/validator-2.priv ]; then
cp /pbft-shared/validators/validator-2.pub /etc/sawtooth/keys/validator.pub
cp /pbft-shared/validators/validator-2.priv /etc/sawtooth/keys/validator.priv
fi &&
if [ ! -e /etc/sawtooth/keys/validator.priv ]; then
sawadm keygen
mkdir -p /pbft-shared/validators || true
cp /etc/sawtooth/keys/validator.pub /pbft-shared/validators/validator-2.pub
cp /etc/sawtooth/keys/validator.priv /pbft-shared/validators/validator-2.priv
fi &&
sawtooth keygen my_key &&
sawtooth-validator -vv \
--endpoint tcp://validator-2:8800 \
--bind component:tcp://eth0:4004 \
--bind consensus:tcp://eth0:5050 \
--bind network:tcp://eth0:8800 \
--scheduler parallel \
--peering static \
--maximum-peer-connectivity 10000 \
--peers tcp://validator-0:8800 \
--peers tcp://validator-1:8800
"
validator-3:
image: hyperledger/sawtooth-validator:nightly
container_name: sawtooth-validator-default-3
expose:
- 4004
- 5050
- 8800
volumes:
- pbft-shared:/pbft-shared
command: |
bash -c "
if [ -e /pbft-shared/validators/validator-3.priv ]; then
cp /pbft-shared/validators/validator-3.pub /etc/sawtooth/keys/validator.pub
cp /pbft-shared/validators/validator-3.priv /etc/sawtooth/keys/validator.priv
fi &&
if [ ! -e /etc/sawtooth/keys/validator.priv ]; then
sawadm keygen
mkdir -p /pbft-shared/validators || true
cp /etc/sawtooth/keys/validator.pub /pbft-shared/validators/validator-3.pub
cp /etc/sawtooth/keys/validator.priv /pbft-shared/validators/validator-3.priv
fi &&
sawtooth keygen my_key &&
sawtooth-validator -vv \
--endpoint tcp://validator-3:8800 \
--bind component:tcp://eth0:4004 \
--bind consensus:tcp://eth0:5050 \
--bind network:tcp://eth0:8800 \
--scheduler parallel \
--peering static \
--maximum-peer-connectivity 10000 \
--peers tcp://validator-0:8800 \
--peers tcp://validator-1:8800 \
--peers tcp://validator-2:8800
"
validator-4:
image: hyperledger/sawtooth-validator:nightly
container_name: sawtooth-validator-default-4
expose:
- 4004
- 5050
- 8800
volumes:
- pbft-shared:/pbft-shared
command: |
bash -c "
if [ -e /pbft-shared/validators/validator-4.priv ]; then
cp /pbft-shared/validators/validator-4.pub /etc/sawtooth/keys/validator.pub
cp /pbft-shared/validators/validator-4.priv /etc/sawtooth/keys/validator.priv
fi &&
if [ ! -e /etc/sawtooth/keys/validator.priv ]; then
sawadm keygen
mkdir -p /pbft-shared/validators || true
cp /etc/sawtooth/keys/validator.pub /pbft-shared/validators/validator-4.pub
cp /etc/sawtooth/keys/validator.priv /pbft-shared/validators/validator-4.priv
fi &&
sawtooth keygen my_key &&
sawtooth-validator -vv \
--endpoint tcp://validator-4:8800 \
--bind component:tcp://eth0:4004 \
--bind consensus:tcp://eth0:5050 \
--bind network:tcp://eth0:8800 \
--scheduler parallel \
--peering static \
--maximum-peer-connectivity 10000 \
--peers tcp://validator-0:8800 \
--peers tcp://validator-1:8800 \
--peers tcp://validator-2:8800 \
--peers tcp://validator-3:8800
"
# -------------=== pbft engines ===-------------
pbft-0:
image: hyperledger/sawtooth-pbft-engine:nightly
container_name: sawtooth-pbft-engine-default-0
command: pbft-engine -vv --connect tcp://validator-0:5050
stop_signal: SIGKILL
pbft-1:
image: hyperledger/sawtooth-pbft-engine:nightly
container_name: sawtooth-pbft-engine-default-1
command: pbft-engine -vv --connect tcp://validator-1:5050
stop_signal: SIGKILL
pbft-2:
image: hyperledger/sawtooth-pbft-engine:nightly
container_name: sawtooth-pbft-engine-default-2
command: pbft-engine -vv --connect tcp://validator-2:5050
stop_signal: SIGKILL
pbft-3:
image: hyperledger/sawtooth-pbft-engine:nightly
container_name: sawtooth-pbft-engine-default-3
command: pbft-engine -vv --connect tcp://validator-3:5050
stop_signal: SIGKILL
pbft-4:
image: hyperledger/sawtooth-pbft-engine:nightly
container_name: sawtooth-pbft-engine-default-4
command: pbft-engine -vv --connect tcp://validator-4:5050
stop_signal: SIGKILL
The code for the transaction processor is below
import sys
from sawtooth_sdk.processor.core import TransactionProcessor
from sawtooth_sdk.processor.handler import TransactionHandler
from transaction_family import TransactionPayload, State, InvalidAction, \
NAMESPACE, NAME, VERSION
import logging
logger = logging.getLogger(__name__)
class CustomTransactionHandler(TransactionHandler):
@property
def family_name(self):
return NAME
@property
def family_versions(self):
return [VERSION]
@property
def namespaces(self):
return [NAMESPACE]
# The argument transaction is an instance of the class Transaction that
# is created from the protobuf definition. Also, context is an instance of
# the class Context from the python SDK.
def apply(self, transaction, context):
logger.error("inside apply")
header = transaction.header
signer = header.signer_public_key
print(transaction.payload)
transaction = TransactionPayload.from_bytes(transaction.payload)
state = State(context)
if transaction.action == 'insert':
state.insert(transaction.key, transaction.value)
pass
elif transaction.action == 'delete':
state.delete(transaction.key)
pass
else:
raise InvalidAction(transaction.action)
def main():
processor = TransactionProcessor(url="tcp://localhost:4004")
processor.add_handler(CustomTransactionHandler())
processor.start()
if __name__ == '__main__':
logging.basicConfig(filename='example.log',
level=logging.DEBUG)
logger.setLevel(logging.INFO)
logger.info("hello")
main()
Transaction Family module is below:
import hashlib
import sys
import cbor2
NAME = 'custom'
NAMESPACE = hashlib.sha512(NAME.encode('utf-8')).hexdigest()[:6]
VERSION = '1.0'
def generate_address(key):
return NAMESPACE + hashlib.sha512(str(key).encode('utf-8')).hexdigest()[
-64:]
class TransactionPayload:
def __init__(self, payload):
action, key, value = cbor2.loads(payload)
self._action = action
self._key = int(key)
self._value = int(value)
@property
def action(self):
return self._action
@property
def key(self):
return self._key
@property
def value(self):
return self._value
@classmethod
def from_bytes(cls, payload):
return cls(payload)
class State:
def __init__(self, context):
self._context = context
def insert(self, key, value):
address = generate_address(key)
n_req_bytes = (value.bit_length() + 7) // 8
self._context.set_state({address: value.to_bytes(n_req_bytes,
sys.byteorder)})
def delete(self, key):
address = generate_address(key)
self._context.delete_state([address])
class InvalidAction(Exception):
def __init__(self, msg):
self._msg = msg
def __str__(self):
return self._msg
Finally, the client through which I am trying to access the rest api is below:
import random
from hashlib import sha512
import requests
import cbor2
from sawtooth_sdk.protobuf.batch_pb2 import BatchHeader, Batch, BatchList
from sawtooth_sdk.protobuf.transaction_pb2 import TransactionHeader, Transaction
from sawtooth_signing import create_context
from sawtooth_signing import CryptoFactory
from transaction_family import generate_address, NAME, VERSION
import secrets
context = create_context('secp256k1')
private_key = context.new_random_private_key()
print(private_key)
signer = CryptoFactory(context).new_signer(private_key)
def insert(key, val):
payload_bytes = cbor2.dumps(['insert', key, val])
address = generate_address(key)
txn_header_bytes = TransactionHeader(
family_name=NAME,
family_version=VERSION,
inputs=[address],
outputs=[address],
signer_public_key=signer.get_public_key().as_hex(),
batcher_public_key=signer.get_public_key().as_hex(),
dependencies=[],
payload_sha512=sha512(payload_bytes).hexdigest(),
nonce=secrets.token_hex(16),
# nonce=hex(random.randint(0, 2**64))
).SerializeToString()
signature = signer.sign(txn_header_bytes)
txn = Transaction(header=txn_header_bytes,
header_signature=signature,
payload=payload_bytes)
txns = [txn]
batch_header_bytes = BatchHeader(
signer_public_key=signer.get_public_key().as_hex(),
transaction_ids=[txn.header_signature for txn in txns]
).SerializeToString()
signature = signer.sign(batch_header_bytes)
batch = Batch(
header=batch_header_bytes,
header_signature=signature,
transactions=txns,
trace=True
)
batch_list_bytes = BatchList(batches=[batch]).SerializeToString()
print(batch_list_bytes)
# send request
resp = requests.post(
'http://localhost:8008/batches',
headers={'Content-Type': 'application/octet-stream'},
data=batch_list_bytes)
print(resp)
print(resp.json())
def main():
insert(1, 2)
if __name__ == '__main__':
main()
I figured out the problem. It was a mistake on my part. I was trying to run only one instance of the transaction processor. However, each node requires an instance of the transaction processor. Once I modified my docker-compose file to include the transaction processor for all the five nodes, it worked as expected.
Posting this answer if anyone else faces a similar issue.