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)