diff --git a/bin/build_all b/bin/build_all index acf0f48..cb9f2a4 100755 --- a/bin/build_all +++ b/bin/build_all @@ -33,7 +33,7 @@ usage() { echo "" echo "Optional build targets:" echo " -d tps,rest,admin" - echo " -p asset,unit, match,setting,rest,admin" + echo " -p asset,unit,match,setting,track,rest,admin" echo "" echo "Examples:" echo " build_all " @@ -76,6 +76,7 @@ PRD_CHOICES=$(echo "hashblock-asset-tp; hashblock-unit-tp; hashblock-match-tp; hashblock-setting-tp; +hashblock-track-tp; hashblock-rest; hashblock-admin;"|tr -d '\n') PRD_LOOKUP="asset;unit;match;setting;rest;admin" diff --git a/bin/protogen b/bin/protogen index f26e26c..8669319 100755 --- a/bin/protogen +++ b/bin/protogen @@ -58,6 +58,10 @@ def main(args=None): proto_dir, "families/setting", "hashblock_setting/protobuf") + protoc_python( + proto_dir, + "families/track", + "hashblock_track/protobuf") def protoc_python(src_dir, base_dir, pkg): diff --git a/bin/track-tp b/bin/track-tp new file mode 100755 index 0000000..487053c --- /dev/null +++ b/bin/track-tp @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +# Copyright 2018 Frank V. Castellucci and Arthur Greef +# +# 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. +# ----------------------------------------------------------------------------- + +import os +import sys + +sys.path.insert(0, os.path.join( + os.path.dirname(os.path.dirname(os.path.realpath(__file__))), + 'families/track/hashblock_track')) + + +from processor.main import main + +if __name__ == '__main__': + main() diff --git a/docker/compose/hashblock-distro.yaml b/docker/compose/hashblock-distro.yaml index dfc15c1..7c49d53 100644 --- a/docker/compose/hashblock-distro.yaml +++ b/docker/compose/hashblock-distro.yaml @@ -86,6 +86,25 @@ services: \"" stop_signal: SIGKILL + track-processor: + image: hashblock/hashblock-track-tp:latest + container_name: hashblock-track-tp-latest + restart: always + volumes: + - ../../../hbruntime:/project/private + environment: + - HASHBLOCK_KEYS=/project/private + - HASHBLOCK_CONFIG=/project/private + - PYTHONPATH=/project/hashblock-exchange + depends_on: + - asset-processor + command: "bash -c \"\ + sleep 1 && \ + track-tp -vv \ + -C tcp://validator:4004 \ + \"" + stop_signal: SIGKILL + hashblock_rest: image: hashblock/hashblock-rest:latest container_name: hashblock-rest diff --git a/docker/compose/hashblock-local.yaml b/docker/compose/hashblock-local.yaml index a56a9ff..ddf5f3a 100644 --- a/docker/compose/hashblock-local.yaml +++ b/docker/compose/hashblock-local.yaml @@ -124,6 +124,24 @@ services: \"" stop_signal: SIGKILL + track-processor: + image: hashblock-dev-generic-tp + container_name: hashblock-dev-track-tp + volumes: + - ../..:/project/hashblock-exchange + environment: + - HASHBLOCK_KEYS=/project/hashblock-exchange/localkeys + - HASHBLOCK_CONFIG=/project/hashblock-exchange/localconfig + - PYTHONPATH=/project/hashblock-exchange + depends_on: + - validator + command: "bash -c \"\ + sleep 1 && + track-tp -vv \ + -C tcp://validator:4004 \ + \"" + stop_signal: SIGKILL + settings-tp: image: hyperledger/sawtooth-settings-tp:latest container_name: sawtooth-settings-tp-local diff --git a/docker/compose/hashblock-node.yaml b/docker/compose/hashblock-node.yaml index 16a8600..15caee0 100644 --- a/docker/compose/hashblock-node.yaml +++ b/docker/compose/hashblock-node.yaml @@ -82,6 +82,25 @@ services: \"" stop_signal: SIGKILL + track-processor: + image: hashblock/hashblock-track-tp:latest + container_name: hashblock-track-tp-latest + restart: always + volumes: + - /sawtooth:/sawtooth + environment: + - HASHBLOCK_KEYS=/sawtooth/keys + - HASHBLOCK_CONFIG=/sawtooth/config + - PYTHONPATH=/project/hashblock-exchange + depends_on: + - asset-processor + command: "bash -c \"\ + sleep 1 && \ + track-tp -vv \ + -C tcp://validator:4004 \ + \"" + stop_signal: SIGKILL + hashblock_rest: image: hashblock/hashblock-rest:latest container_name: hashblock-rest diff --git a/docker/hashblock-track-tp b/docker/hashblock-track-tp new file mode 100644 index 0000000..c32e7fa --- /dev/null +++ b/docker/hashblock-track-tp @@ -0,0 +1,94 @@ + +# ------------------------------------------------------------------------------ +# Copyright 2018 Frank V. Castellucci and Arthur Greef +# +# 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. +# ------------------------------------------------------------------------------ + +# Description: +# Builds a deployable production image for our hashblock-track-tp +# +# base image: +# 1. Installs protobuf +# 2. Generates protobuf files +# final image: +# 1. Installs sawtooth-sdk and crypto +# 2. Copies relevant files from base +# Build: +# $ cd hashblock-exchange +# $ docker build . -f docker/hashblock-track-tp -t hashblock/hashblock-track-tp:latest +# + +FROM ubuntu:xenial as base +LABEL maintainers="Frank V. Castellucci, Arthur Greef" + +RUN apt-get update \ + && apt-get install -y -q \ + apt-transport-https \ + python3-dev \ + python3-pip + +RUN mkdir -p /builder && \ + pip3 install \ + grpcio-tools \ + grpcio && \ + rm -rf /var/lib/apt/lists/ + +WORKDIR /builder + +COPY bin bin +COPY apps apps +COPY modules modules +COPY protos protos +COPY families families + +RUN bin/protogen && \ + pip3 uninstall -y grpcio grpcio-tools + +# The final image + +FROM ubuntu:xenial +LABEL maintainers="Frank V. Castellucci, Arthur Greef" + +RUN apt-get update \ + && apt-get install -y -q \ + apt-transport-https \ + libssl-dev \ + libffi-dev \ + python3-dev \ + python3-pip \ + build-essential \ + automake \ + pkg-config \ + libtool \ + libffi-dev + +RUN pip3 install eciespy cryptography && \ + SECP_BUNDLED_EXPERIMENTAL=1 pip3 --no-cache-dir install --no-binary secp256k1 secp256k1 && \ + pip3 install sawtooth-sdk --upgrade && \ + rm -r /root/.cache && \ + apt-get remove -y build-essential automake && apt-get autoremove -y + +RUN mkdir -p /project/hashblock-exchange && \ + mkdir -p /project/hashblock-exchange/bin && \ + mkdir -p /project/hashblock-exchange/modules && \ + mkdir -p /project/hashblock-exchange/families/track && \ + mkdir -p /var/log/sawtooth + +COPY --from=base /builder/bin/track-tp /project/hashblock-exchange/bin +COPY libs/hbzksnark /project/hashblock-exchange/bin +COPY --from=base /builder/modules /project/hashblock-exchange/modules +COPY --from=base /builder/families/track/hashblock_track /project/hashblock-exchange/families/track/hashblock_track + +WORKDIR /project/hashblock-exchange +ENV PATH $PATH:/project/hashblock-exchange/bin diff --git a/families/track/__init__.py b/families/track/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/families/track/hashblock_track/__init__.py b/families/track/hashblock_track/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/families/track/hashblock_track/processor/__init__.py b/families/track/hashblock_track/processor/__init__.py new file mode 100644 index 0000000..5a33ee4 --- /dev/null +++ b/families/track/hashblock_track/processor/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2017 Intel Corporation +# +# 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. +# ------------------------------------------------------------------------------ diff --git a/families/track/hashblock_track/processor/config/__init__.py b/families/track/hashblock_track/processor/config/__init__.py new file mode 100644 index 0000000..044c30a --- /dev/null +++ b/families/track/hashblock_track/processor/config/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2017 Frank V. Castellucci +# +# 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. +# ------------------------------------------------------------------------------ diff --git a/families/track/hashblock_track/processor/config/track.py b/families/track/hashblock_track/processor/config/track.py new file mode 100644 index 0000000..0899b37 --- /dev/null +++ b/families/track/hashblock_track/processor/config/track.py @@ -0,0 +1,128 @@ +# ------------------------------------------------------------------------------ +# Copyright 2018 Frank V. Castellucci and Arthur Greef +# +# 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. +# ------------------------------------------------------------------------------ + +import collections +import logging +import os + +import toml + +from sawtooth_sdk.processor.exceptions import LocalConfigurationError + +LOGGER = logging.getLogger(__name__) + + +def load_default_track_config(): + """ + Returns the default TrackConfig + """ + return TrackConfig( + connect='tcp://localhost:4004' + ) + + +def load_toml_track_config(filename): + """Returns a TrackConfig created by loading a TOML file from the + filesystem. + + Args: + filename (string): The name of the file to load the config from + + Returns: + config (TrackConfig): The TrackConfig created from the stored + toml file. + + Raises: + LocalConfigurationError + """ + if not os.path.exists(filename): + LOGGER.info( + "Skipping transaction proccesor config loading from non-existent" + " config file: %s", filename) + return TrackConfig() + + LOGGER.info("Loading transaction processor information from config: %s", + filename) + + try: + with open(filename) as fd: + raw_config = fd.read() + except IOError as e: + raise LocalConfigurationError( + "Unable to load transaction processor configuration file:" + " {}".format(str(e))) + + toml_config = toml.loads(raw_config) + invalid_keys = set(toml_config.keys()).difference( + ['connect']) + if invalid_keys: + raise LocalConfigurationError( + "Invalid keys in transaction processor config: " + "{}".format(", ".join(sorted(list(invalid_keys))))) + + config = TrackConfig( + connect=toml_config.get("connect", None) + ) + + return config + + +def merge_track_config(configs): + """ + Given a list of TrackConfig objects, merges them into a single + TrackConfig, giving priority in the order of the configs + (first has highest priority). + + Args: + config (list of TrackConfigs): The list of units configs that + must be merged together + + Returns: + config (TrackConfig): one TrackConfig that combines all of the + passed in configs. + """ + connect = None + + for config in reversed(configs): + if config.connect is not None: + connect = config.connect + + return TrackConfig(connect=connect) + + +class TrackConfig: + def __init__(self, connect=None): + self._connect = connect + + @property + def connect(self): + return self._connect + + def __repr__(self): + # not including password for opentsdb + return \ + "{}(connect={})".format( + self.__class__.__name__, + repr(self._connect), + ) + + def to_dict(self): + return collections.OrderedDict([ + ('connect', self._connect), + ]) + + def to_toml_string(self): + return str(toml.dumps(self.to_dict())).strip().split('\n') diff --git a/families/track/hashblock_track/processor/handler.py b/families/track/hashblock_track/processor/handler.py new file mode 100644 index 0000000..1c6c438 --- /dev/null +++ b/families/track/hashblock_track/processor/handler.py @@ -0,0 +1,105 @@ +# ------------------------------------------------------------------------------ +# Copyright 2018 Frank V. Castellucci and Arthur Greef +# +# 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. +# ------------------------------------------------------------------------------ + +import logging + +from sawtooth_sdk.processor.handler import TransactionHandler +from sawtooth_sdk.messaging.future import FutureTimeoutError +from sawtooth_sdk.processor.exceptions import InvalidTransaction +from sawtooth_sdk.processor.exceptions import InternalError + +from protobuf.track_pb2 import TrackPayload +from protobuf.track_pb2 import Track + +from modules.address import Address + +LOGGER = logging.getLogger(__name__) + +# Number of seconds to wait for state operations to succeed +STATE_TIMEOUT_SEC = 10 + + +class TrackTransactionHandler(TransactionHandler): + + def __init__(self): + self._addresser = Address.track_addresser() + self._auth_list = None + self._action = None + + @property + def addresser(self): + return self._addresser + + @property + def family_name(self): + return self.addresser.family_ns_name + + @property + def family_versions(self): + return self.addresser.family_versions + + @property + def namespaces(self): + return [self.addresser.family_ns_hash] + + def apply(self, transaction, context): + track_payload = TrackPayload() + track_payload.ParseFromString(transaction.payload) + track = Track() + track.ParseFromString(track_payload.data) + address = self.addresser.track(track.asset_ident, track.property) + + return self._set_track(context, track, address) + + def _set_track(self, context, track, address): + """Change the hashblock tracks on the block + """ + LOGGER.debug("Processing track payload") + + try: + context.set_state( + {address: track.SerializeToString()}, + timeout=STATE_TIMEOUT_SEC) + except FutureTimeoutError: + LOGGER.warning( + 'Timeout occured on set_state([%s, ])', + self.address) + raise InternalError( + 'Unable to set {}'.format(self.address)) + if self.action == TrackPayload.CREATE: + pass + + +def _get_track(context, address, default_value=None): + """Get a hashblock tracks from the block + """ + track = Track() + results = _get_state(context, address) + if results: + track.ParseFromString(results[0].data) + return track + return default_value + + +def _get_state(context, address): + try: + results = context.get_state([address], timeout=STATE_TIMEOUT_SEC) + except FutureTimeoutError: + LOGGER.warning( + 'Timeout occured on context.get_state([%s])', + address) + raise InternalError('Unable to get {}'.format(address)) + return results diff --git a/families/track/hashblock_track/processor/main.py b/families/track/hashblock_track/processor/main.py new file mode 100644 index 0000000..2ce799b --- /dev/null +++ b/families/track/hashblock_track/processor/main.py @@ -0,0 +1,168 @@ +# ------------------------------------------------------------------------------ +# Copyright 2018 Frank V. Castellucci and Arthur Greef +# +# 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. +# ------------------------------------------------------------------------------ + +import argparse +import logging +import os +import sys +import pkg_resources + +from colorlog import ColoredFormatter + +# ResourceTransactionHandler + +from sawtooth_sdk.processor.core import TransactionProcessor +from sawtooth_sdk.processor.log import init_console_logging +from sawtooth_sdk.processor.log import log_configuration +from sawtooth_sdk.processor.config import get_log_config +from sawtooth_sdk.processor.config import get_log_dir +from sawtooth_sdk.processor.config import get_config_dir +from processor.handler import TrackTransactionHandler +from processor.config.track import ( + TrackConfig, + load_default_track_config, + load_toml_track_config, + merge_track_config) + +DISTRIBUTION_NAME = 'hashblock-track' + + +def create_console_handler(verbose_level): + clog = logging.StreamHandler() + formatter = ColoredFormatter( + "%(log_color)s[%(asctime)s.%(msecs)03d " + "%(levelname)-8s %(module)s]%(reset)s " + "%(white)s%(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + reset=True, + log_colors={ + 'DEBUG': 'cyan', + 'INFO': 'green', + 'WARNING': 'yellow', + 'ERROR': 'red', + 'CRITICAL': 'red', + }) + + clog.setFormatter(formatter) + + if verbose_level == 0: + clog.setLevel(logging.WARN) + elif verbose_level == 1: + clog.setLevel(logging.INFO) + else: + clog.setLevel(logging.DEBUG) + + return clog + + +def setup_loggers(verbose_level, processor): + log_config = get_log_config(filename="track_log_config.toml") + + # If no toml, try loading yaml + if log_config is None: + log_config = get_log_config(filename="track_log_config.yaml") + + if log_config is not None: + log_configuration(log_config=log_config) + else: + log_dir = get_log_dir() + # use the transaction processor zmq identity for filename + log_configuration( + log_dir=log_dir, + name="track-" + str(processor.zmq_id)[2:-1]) + + init_console_logging(verbose_level=verbose_level) + + +def create_parser(prog_name): + parser = argparse.ArgumentParser( + prog=prog_name, + description='Starts a hashblock-track transaction processor.', + epilog='This process is required to track oracle updates ', + formatter_class=argparse.RawDescriptionHelpFormatter) + + parser.add_argument( + '-C', '--connect', + help='specify the endpoint for the validator connection (default: ' + 'tcp://localhost:4004) ') + + parser.add_argument( + '-v', '--verbose', + action='count', + default=0, + help='enable more verbose output to stderr') + + try: + version = pkg_resources.get_distribution(DISTRIBUTION_NAME).version + except pkg_resources.DistributionNotFound: + version = 'UNKNOWN' + + parser.add_argument( + '-V', '--version', + action='version', + version=(DISTRIBUTION_NAME + ' (Hashblock Exchange) version {}') + .format(version), + help='display version information') + + return parser + + +def load_tracks_config(first_config): + default_tracks_config = load_default_track_config() + conf_file = os.path.join(get_config_dir(), 'track.toml') + toml_config = load_toml_track_config(conf_file) + return merge_track_config( + configs=[first_config, toml_config, default_tracks_config]) + + +def create_tracks_config(args): + return TrackConfig(connect=args.connect) + + +def main(prog_name=os.path.basename(sys.argv[0]), args=None, + with_loggers=True): + if args is None: + args = sys.argv[1:] + parser = create_parser(prog_name) + args = parser.parse_args(args) + + arg_config = create_tracks_config(args) + track_config = load_tracks_config(arg_config) + processor = TransactionProcessor(url=track_config.connect) + + if with_loggers is True: + if args.verbose is None: + verbose_level = 0 + else: + verbose_level = args.verbose + setup_loggers(verbose_level=verbose_level, processor=processor) + + my_logger = logging.getLogger(__name__) + my_logger.debug("Processor loaded") + + handler = TrackTransactionHandler() + + processor.add_handler(handler) + + my_logger.debug( + "Hashblock track instantiated, starting track processor thread...") + + try: + processor.start() + except KeyboardInterrupt: + pass + finally: + processor.stop() diff --git a/families/track/nose2.cfg b/families/track/nose2.cfg new file mode 100644 index 0000000..0a7410d --- /dev/null +++ b/families/track/nose2.cfg @@ -0,0 +1,22 @@ +# Copyright 2017 Intel Corporation +# +# 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. +# ------------------------------------------------------------------------------ +[unittest] +start-dir = tests +code-directories = .. +test-file-pattern = test*.py +plugins = nose2.plugins.coverage + +[coverage] +always-on = True diff --git a/families/track/tests/hashblock_track_test/__init__.py b/families/track/tests/hashblock_track_test/__init__.py new file mode 100644 index 0000000..13db520 --- /dev/null +++ b/families/track/tests/hashblock_track_test/__init__.py @@ -0,0 +1,15 @@ +# ------------------------------------------------------------------------------ +# Copyright 2018 Frank V. Castellucci +# +# 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. +# ------------------------------------------------------------------------------ diff --git a/families/track/tests/hashblock_track_test/track_message_factory.py b/families/track/tests/hashblock_track_test/track_message_factory.py new file mode 100644 index 0000000..3025de2 --- /dev/null +++ b/families/track/tests/hashblock_track_test/track_message_factory.py @@ -0,0 +1,81 @@ +# ------------------------------------------------------------------------------ +# Copyright 2018 Frank V. Castellucci and Arthur Greef +# +# 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. +# ------------------------------------------------------------------------------ + +from sawtooth_processor_test.message_factory import MessageFactory + +from protobuf.setting_pb2 import SettingPayload +from protobuf.setting_pb2 import Settings +from modules.address import Address + + +class SettingMessageFactory(object): + + def __init__(self, signer=None): + self._asset_addr = Address(Address.FAMILY_ASSET) + self._setting_addr = Address(Address.FAMILY_SETTING) + self._factory = MessageFactory( + family_name=Address.NAMESPACE_SETTING, + family_version="0.1.0", + namespace=[self._setting_addr.ns_family], + signer=signer) + + @property + def public_key(self): + return self._factory.get_public_key() + + def create_tp_register(self): + return self._factory.create_tp_register() + + def create_tp_response(self, status): + return self._factory.create_tp_response(status) + + def _create_tp_process_request(self, dimension, payload): + address = self._setting_addr.settings(dimension) + inputs = [address, self._asset_addr.candidates(dimension)] + outputs = [address, self._asset_addr.candidates(dimension)] + return self._factory.create_tp_process_request( + payload.SerializeToString(), inputs, outputs, []) + + def create_payload_request(self, settings, dimension, action): + payload = SettingPayload( + action=action, + dimension=dimension, + data=settings.SerializeToString()) + return self._create_tp_process_request(dimension, payload) + + def create_setting_transaction(self, auth_keys, thresh, dimension, action): + setting = Settings(auth_list=auth_keys, threshold=thresh) + return self.create_payload_request(setting, dimension, action) + + def create_get_request(self, address): + return self._factory.create_get_request([address]) + + def create_get_response(self, address, data=None): + return self._factory.create_get_response({address: data}) + + def create_set_request(self, address, setting=None): + return self._factory.create_set_request({address: setting}) + + def create_set_response(self, address): + return self._factory.create_set_response([address]) + + def create_add_event_request(self, key): + return self._factory.create_add_event_request( + "hashblock.setting/update", + [("updated", key)]) + + def create_add_event_response(self): + return self._factory.create_add_event_response() diff --git a/families/track/tests/test_tp_track.py b/families/track/tests/test_tp_track.py new file mode 100644 index 0000000..5f42ba7 --- /dev/null +++ b/families/track/tests/test_tp_track.py @@ -0,0 +1,165 @@ +# ------------------------------------------------------------------------------ +# Copyright 2018 Frank V. Castellucci and Arthur Greef +# +# 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. +# ------------------------------------------------------------------------------ + +from protobuf.asset_pb2 import AssetCandidates +from protobuf.setting_pb2 import Settings +from protobuf.setting_pb2 import SettingPayload + +from modules.address import Address + +from hashblock_setting_test.setting_message_factory \ + import SettingMessageFactory + +from sawtooth_processor_test.transaction_processor_test_case \ + import TransactionProcessorTestCase + + +VOTER2 = "59c272cb554c7100dd6c1e38b5c77f158146be29373329e503bfcb81e70d1ddd" +EMPTY_CANDIDATES = AssetCandidates(candidates=[]).SerializeToString() + +_asset_addr = Address(Address.FAMILY_ASSET) +_setting_addr = Address(Address.FAMILY_SETTING) + + +class TestSetting(TransactionProcessorTestCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.factory = SettingMessageFactory() + cls.setting = Settings( + auth_list=','.join([cls.factory.public_key, VOTER2]), + threshold='2') + + def _expect_get(self, address, data=None): + received = self.validator.expect( + self.factory.create_get_request(address)) + self.validator.respond( + self.factory.create_get_response(address, data), + received) + + def _expect_set(self, address, expected_value): + received = self.validator.expect( + self.factory.create_set_request(address, expected_value)) + self.validator.respond( + self.factory.create_set_response(address), received) + + def _expect_add_event(self, address): + received = self.validator.expect( + self.factory.create_add_event_request(address)) + + self.validator.respond( + self.factory.create_add_event_response(), + received) + + def _expect_ok(self): + self.validator.expect(self.factory.create_tp_response("OK")) + + def _expect_invalid_transaction(self): + self.validator.expect( + self.factory.create_tp_response("INVALID_TRANSACTION")) + + def _expect_internal_error(self): + self.validator.expect( + self.factory.create_tp_response("INTERNAL_ERROR")) + + def _get_empty_candidates(self, dimension): + self._expect_get( + _asset_addr.candidates(dimension), + EMPTY_CANDIDATES) + + def _set_default_settings(self, dimension, action): + self.validator.send(self.factory.create_payload_request( + self.setting, dimension, action)) + + def _set_setting(self, auth_list, threshold, dimension, action): + self.validator.send(self.factory.create_setting_transaction( + auth_list, threshold, dimension, action)) + + @property + def _public_key(self): + return self.factory.public_key + + def test_valid_settings(self): + """Sunny day settings create + """ + # Start the transaction + self._set_default_settings( + Address.DIMENSION_UNIT, + SettingPayload.CREATE) + # Fetch an empty authorization/threshold setting + self._expect_get( + _setting_addr.settings(Address.DIMENSION_UNIT)) + # Set the settings + self._expect_set( + _setting_addr.settings(Address.DIMENSION_UNIT), + self.setting.SerializeToString()) + # Set the candidates + self._expect_set( + _asset_addr.candidates(Address.DIMENSION_UNIT), + EMPTY_CANDIDATES) + self._expect_ok() + + def test_setting_create_exist(self): + """Test behavior of create when settings exist + """ + self._set_default_settings( + Address.DIMENSION_UNIT, + SettingPayload.CREATE) + # Fetch an empty authorization/threshold setting + self._expect_get( + _setting_addr.settings(Address.DIMENSION_UNIT), + self.setting.SerializeToString()) + self._expect_invalid_transaction() + + def test_bad_threshold(self): + """Test when threshold is not a number + """ + self._set_setting( + ','.join([self._public_key, VOTER2]), + "", + Address.DIMENSION_UNIT, + SettingPayload.CREATE) + # Fetch an empty authorization/threshold setting + self._expect_get( + _setting_addr.settings(Address.DIMENSION_UNIT)) + self._expect_invalid_transaction() + + def test_threshold_too_small(self): + """Test a threshold less than or equal to zero + """ + self._set_setting( + ','.join([self._public_key, VOTER2, VOTER2]), + "0", + Address.DIMENSION_UNIT, + SettingPayload.CREATE) + # Fetch an empty authorization/threshold setting + self._expect_get( + _setting_addr.settings(Address.DIMENSION_UNIT)) + self._expect_invalid_transaction() + + def test_authlist_too_small(self): + """Test when threshold exceeds size of authorization list + """ + self._set_setting( + ','.join([self._public_key, VOTER2]), + "3", + Address.DIMENSION_UNIT, + SettingPayload.CREATE) + # Fetch an empty authorization/threshold setting + self._expect_get( + _setting_addr.settings(Address.DIMENSION_UNIT)) + self._expect_invalid_transaction() diff --git a/families/track/tests/test_tp_track.yaml b/families/track/tests/test_tp_track.yaml new file mode 100644 index 0000000..086a3bb --- /dev/null +++ b/families/track/tests/test_tp_track.yaml @@ -0,0 +1,51 @@ +# ------------------------------------------------------------------------------ +# Copyright 2018 Frank V. Castellucci and Arthur Greef +# +# 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: "2.1" + +services: + + settings-processor: + image: hashblock-dev-generic-tp:latest + container_name: hashblock-setting-dev-tp + volumes: + - ../../..:/project/hashblock-exchange + environment: + - PYTHONPATH=/project/hashblock-exchange + expose: + - 4004 + command: setting-tp -vv -C tcp://test-tp-setting:4004 + stop_signal: SIGKILL + + + test-tp-setting: + image: hashblock-python-testing:latest + volumes: + - ../../..:/project/hashblock-exchange + expose: + - 4004 + command: nose2 + -c /project/hashblock-exchange/families/setting/nose2.cfg + -v + -s /project/hashblock-exchange/families/setting/tests + test_tp_setting + stop_signal: SIGKILL + environment: + TEST_BIND: "tcp://eth0:4004" + PYTHONPATH: "/project/hashblock-exchange/families/setting/hashblock_setting:\ + /project/hashblock-exchange:\ + /project/hashblock-exchange/families/setting/tests:\ + /project/hashblock-exchange/sdk/python" diff --git a/modules/address.py b/modules/address.py index 2d230ad..1d72556 100644 --- a/modules/address.py +++ b/modules/address.py @@ -31,6 +31,7 @@ class Address(ABC): FAMILY_ASSET = "asset" FAMILY_MATCH = "match" FAMILY_SETTING = "setting" + FAMILY_TRACK = "track" # Dimensions, used by families MATCH_TYPE_UTXQ = "utxq" @@ -55,6 +56,10 @@ class Address(ABC): def setting_addresser(cls): return SettingAddress() + @classmethod + def track_addresser(cls): + return TrackAddress() + @classmethod def unit_addresser(cls): return UnitAddress() @@ -188,6 +193,32 @@ def settings(self, stype): + self._filler_hash26 +class TrackAddress(BaseAddress): + """TrackAddress provides the track TP address support + + oracle observations of some property of an asset + """ + def __init__(self): + super().__init__(self.FAMILY_TRACK, ["0.1.0"]) + + # E.g. hashblock.track.???.??? + # 0-2 namespace 6/6 + # 3-5 family 6/12 + # 6-28 ident (from asset) 44/56 + # 28-34 property 14/70 + def track(self, ident, property): + """Create the stype (asset/unit) settings address using key + """ + if ident is None or len(ident) != 44: + raise AssetIdRange( + "Invalid ident {} for {} {} {}" + .format(ident, self._family, ident, property)) + + return self.family_ns_hash \ + + ident \ + + self.hashup(property)[0:14] + + class VotingAddress(BaseAddress): """VotingAddress provides the setting and candidate addresses""" def __init__(self, family, version_list): diff --git a/protos/track.proto b/protos/track.proto new file mode 100644 index 0000000..53d72ea --- /dev/null +++ b/protos/track.proto @@ -0,0 +1,51 @@ +// ----------------------------------------------------------------------------- +// Copyright 2018 Frank V. Castellucci +// +// 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. +// ----------------------------------------------------------------------------- + +syntax = "proto3"; +option java_multiple_files = true; +option java_package = "hashblock.setting.protobuf"; + +// Track data container for on-chain oracle tracking + +message Track { + + // Property being tracked + string property = 1; + + // Asset prime ID + string asset_ident = 2; + + // range proof + bytes proof = 3; + +} + +// Track Payload + +message TrackPayload { + // The action indicates data is contained within this payload + enum Action { + // + TRACK = 0; + } + // The action of this payload + Action action = 1; + + // The track content of this payload + bytes data = 2; +} + +