github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-core-master/families/settings/sawtooth_settings/processor/handler.py (about)

     1  # Copyright 2017 Intel Corporation
     2  #
     3  # Licensed under the Apache License, Version 2.0 (the "License");
     4  # you may not use this file except in compliance with the License.
     5  # You may obtain a copy of the License at
     6  #
     7  #     http://www.apache.org/licenses/LICENSE-2.0
     8  #
     9  # Unless required by applicable law or agreed to in writing, software
    10  # distributed under the License is distributed on an "AS IS" BASIS,
    11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  # See the License for the specific language governing permissions and
    13  # limitations under the License.
    14  # ------------------------------------------------------------------------------
    15  
    16  import logging
    17  import hashlib
    18  import base64
    19  from functools import lru_cache
    20  
    21  
    22  from sawtooth_sdk.processor.handler import TransactionHandler
    23  from sawtooth_sdk.messaging.future import FutureTimeoutError
    24  from sawtooth_sdk.processor.exceptions import InvalidTransaction
    25  from sawtooth_sdk.processor.exceptions import InternalError
    26  
    27  from sawtooth_settings.protobuf.settings_pb2 import SettingsPayload
    28  from sawtooth_settings.protobuf.settings_pb2 import SettingProposal
    29  from sawtooth_settings.protobuf.settings_pb2 import SettingVote
    30  from sawtooth_settings.protobuf.settings_pb2 import SettingCandidate
    31  from sawtooth_settings.protobuf.settings_pb2 import SettingCandidates
    32  from sawtooth_settings.protobuf.setting_pb2 import Setting
    33  
    34  LOGGER = logging.getLogger(__name__)
    35  
    36  
    37  # The config namespace is special: it is not derived from a hash.
    38  SETTINGS_NAMESPACE = '000000'
    39  
    40  # Number of seconds to wait for state operations to succeed
    41  STATE_TIMEOUT_SEC = 10
    42  
    43  
    44  class SettingsTransactionHandler(TransactionHandler):
    45      @property
    46      def family_name(self):
    47          return 'sawtooth_settings'
    48  
    49      @property
    50      def family_versions(self):
    51          return ['1.0']
    52  
    53      @property
    54      def namespaces(self):
    55          return [SETTINGS_NAMESPACE]
    56  
    57      def apply(self, transaction, context):
    58  
    59          txn_header = transaction.header
    60          public_key = txn_header.signer_public_key
    61  
    62          auth_keys = _get_auth_keys(context)
    63          if auth_keys and public_key not in auth_keys:
    64              raise InvalidTransaction(
    65                  '{} is not authorized to change settings'.format(public_key))
    66  
    67          settings_payload = SettingsPayload()
    68          settings_payload.ParseFromString(transaction.payload)
    69  
    70          if settings_payload.action == SettingsPayload.PROPOSE:
    71              return self._apply_proposal(
    72                  auth_keys, public_key, settings_payload.data, context)
    73          elif settings_payload.action == SettingsPayload.VOTE:
    74              return self._apply_vote(public_key, settings_payload.data,
    75                                      auth_keys, context)
    76          else:
    77              raise InvalidTransaction(
    78                  "'action' must be one of {PROPOSE, VOTE} in 'Ballot' mode")
    79  
    80      def _apply_proposal(self, auth_keys, public_key,
    81                          setting_proposal_data, context):
    82          setting_proposal = SettingProposal()
    83          setting_proposal.ParseFromString(setting_proposal_data)
    84  
    85          proposal_id = hashlib.sha256(setting_proposal_data).hexdigest()
    86  
    87          approval_threshold = _get_approval_threshold(context)
    88  
    89          _validate_setting(auth_keys,
    90                            setting_proposal.setting,
    91                            setting_proposal.value)
    92  
    93          if approval_threshold > 1:
    94              setting_candidates = _get_setting_candidates(context)
    95  
    96              existing_candidate = _first(
    97                  setting_candidates.candidates,
    98                  lambda candidate: candidate.proposal_id == proposal_id)
    99  
   100              if existing_candidate is not None:
   101                  raise InvalidTransaction(
   102                      'Duplicate proposal for {}'.format(
   103                          setting_proposal.setting))
   104  
   105              record = SettingCandidate.VoteRecord(
   106                  public_key=public_key,
   107                  vote=SettingVote.ACCEPT)
   108              setting_candidates.candidates.add(
   109                  proposal_id=proposal_id,
   110                  proposal=setting_proposal,
   111                  votes=[record]
   112              )
   113  
   114              LOGGER.debug('Proposal made to set %s to %s',
   115                           setting_proposal.setting,
   116                           setting_proposal.value)
   117              _save_setting_candidates(context, setting_candidates)
   118          else:
   119              _set_setting_value(context,
   120                                 setting_proposal.setting,
   121                                 setting_proposal.value)
   122  
   123      def _apply_vote(self, public_key,
   124                      settings_vote_data, authorized_keys, context):
   125          settings_vote = SettingVote()
   126          settings_vote.ParseFromString(settings_vote_data)
   127          proposal_id = settings_vote.proposal_id
   128  
   129          setting_candidates = _get_setting_candidates(context)
   130          candidate = _first(
   131              setting_candidates.candidates,
   132              lambda candidate: candidate.proposal_id == proposal_id)
   133  
   134          if candidate is None:
   135              raise InvalidTransaction(
   136                  "Proposal {} does not exist.".format(proposal_id))
   137  
   138          candidate_index = _index_of(setting_candidates.candidates, candidate)
   139  
   140          approval_threshold = _get_approval_threshold(context)
   141  
   142          vote_record = _first(candidate.votes,
   143                               lambda record: record.public_key == public_key)
   144          if vote_record is not None:
   145              raise InvalidTransaction(
   146                  '{} has already voted'.format(public_key))
   147  
   148          candidate.votes.add(
   149              public_key=public_key,
   150              vote=settings_vote.vote)
   151  
   152          accepted_count = 0
   153          rejected_count = 0
   154          for vote_record in candidate.votes:
   155              if vote_record.vote == SettingVote.ACCEPT:
   156                  accepted_count += 1
   157              elif vote_record.vote == SettingVote.REJECT:
   158                  rejected_count += 1
   159  
   160          if accepted_count >= approval_threshold:
   161              _set_setting_value(context,
   162                                 candidate.proposal.setting,
   163                                 candidate.proposal.value)
   164              del setting_candidates.candidates[candidate_index]
   165          elif rejected_count >= approval_threshold or \
   166                  (rejected_count + accepted_count) == len(authorized_keys):
   167              LOGGER.debug('Proposal for %s was rejected',
   168                           candidate.proposal.setting)
   169              del setting_candidates.candidates[candidate_index]
   170          else:
   171              LOGGER.debug('Vote recorded for %s',
   172                           candidate.proposal.setting)
   173  
   174          _save_setting_candidates(context, setting_candidates)
   175  
   176  
   177  def _get_setting_candidates(context):
   178      value = _get_setting_value(context, 'sawtooth.settings.vote.proposals')
   179      if not value:
   180          return SettingCandidates(candidates={})
   181  
   182      setting_candidates = SettingCandidates()
   183      setting_candidates.ParseFromString(base64.b64decode(value))
   184      return setting_candidates
   185  
   186  
   187  def _save_setting_candidates(context, setting_candidates):
   188      _set_setting_value(context,
   189                         'sawtooth.settings.vote.proposals',
   190                         base64.b64encode(
   191                             setting_candidates.SerializeToString()))
   192  
   193  
   194  def _get_approval_threshold(context):
   195      return int(_get_setting_value(
   196          context, 'sawtooth.settings.vote.approval_threshold', 1))
   197  
   198  
   199  def _get_auth_keys(context):
   200      value = _get_setting_value(
   201          context, 'sawtooth.settings.vote.authorized_keys', '')
   202      return _split_ignore_empties(value)
   203  
   204  
   205  def _split_ignore_empties(value):
   206      return [v.strip() for v in value.split(',') if v]
   207  
   208  
   209  def _validate_setting(auth_keys, setting, value):
   210      if not auth_keys and \
   211              setting != 'sawtooth.settings.vote.authorized_keys':
   212          raise InvalidTransaction(
   213              'Cannot set {} until authorized_keys is set.'.format(setting))
   214  
   215      if setting == 'sawtooth.settings.vote.authorized_keys':
   216          if not _split_ignore_empties(value):
   217              raise InvalidTransaction('authorized_keys must not be empty.')
   218  
   219      if setting == 'sawtooth.settings.vote.approval_threshold':
   220          threshold = None
   221          try:
   222              threshold = int(value)
   223          except ValueError:
   224              raise InvalidTransaction('approval_threshold must be an integer')
   225  
   226          if threshold > len(auth_keys):
   227              raise InvalidTransaction(
   228                  'approval_threshold must be less than or equal to number of '
   229                  'authorized_keys')
   230  
   231      if setting == 'sawtooth.settings.vote.proposals':
   232          raise InvalidTransaction(
   233              'Setting sawtooth.settings.vote.proposals is read-only')
   234  
   235  
   236  def _get_setting_value(context, key, default_value=None):
   237      address = _make_settings_key(key)
   238      setting = _get_setting_entry(context, address)
   239      for entry in setting.entries:
   240          if key == entry.key:
   241              return entry.value
   242  
   243      return default_value
   244  
   245  
   246  def _set_setting_value(context, key, value):
   247      address = _make_settings_key(key)
   248      setting = _get_setting_entry(context, address)
   249  
   250      old_value = None
   251      old_entry_index = None
   252      for i, entry in enumerate(setting.entries):
   253          if key == entry.key:
   254              old_value = entry.value
   255              old_entry_index = i
   256  
   257      if old_entry_index is not None:
   258          setting.entries[old_entry_index].value = value
   259      else:
   260          setting.entries.add(key=key, value=value)
   261  
   262      try:
   263          addresses = list(context.set_state(
   264              {address: setting.SerializeToString()},
   265              timeout=STATE_TIMEOUT_SEC))
   266      except FutureTimeoutError:
   267          LOGGER.warning(
   268              'Timeout occured on context.set_state([%s, <value>])', address)
   269          raise InternalError('Unable to set {}'.format(key))
   270  
   271      if len(addresses) != 1:
   272          LOGGER.warning(
   273              'Failed to save value on address %s', address)
   274          raise InternalError(
   275              'Unable to save config value {}'.format(key))
   276      if setting != 'sawtooth.settings.vote.proposals':
   277          LOGGER.info('Setting setting %s changed from %s to %s',
   278                      key, old_value, value)
   279      context.add_event(
   280          event_type="settings/update",
   281          attributes=[("updated", key)])
   282  
   283  
   284  def _get_setting_entry(context, address):
   285      setting = Setting()
   286  
   287      try:
   288          entries_list = context.get_state([address], timeout=STATE_TIMEOUT_SEC)
   289      except FutureTimeoutError:
   290          LOGGER.warning('Timeout occured on context.get_state([%s])', address)
   291          raise InternalError('Unable to get {}'.format(address))
   292  
   293      if entries_list:
   294          setting.ParseFromString(entries_list[0].data)
   295  
   296      return setting
   297  
   298  
   299  def _to_hash(value):
   300      return hashlib.sha256(value.encode()).hexdigest()
   301  
   302  
   303  def _first(a_list, pred):
   304      return next((x for x in a_list if pred(x)), None)
   305  
   306  
   307  def _index_of(iterable, obj):
   308      return next((i for i, x in enumerate(iterable) if x == obj), -1)
   309  
   310  
   311  _MAX_KEY_PARTS = 4
   312  _ADDRESS_PART_SIZE = 16
   313  _EMPTY_PART = _to_hash('')[:_ADDRESS_PART_SIZE]
   314  
   315  
   316  @lru_cache(maxsize=128)
   317  def _make_settings_key(key):
   318      # split the key into 4 parts, maximum
   319      key_parts = key.split('.', maxsplit=_MAX_KEY_PARTS - 1)
   320      # compute the short hash of each part
   321      addr_parts = [_to_hash(x)[:_ADDRESS_PART_SIZE] for x in key_parts]
   322      # pad the parts with the empty hash, if needed
   323      addr_parts.extend([_EMPTY_PART] * (_MAX_KEY_PARTS - len(addr_parts)))
   324  
   325      return SETTINGS_NAMESPACE + ''.join(addr_parts)