github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-core-master/cli/sawtooth_cli/sawset.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 argparse 17 from base64 import b64decode 18 import csv 19 import datetime 20 import getpass 21 import hashlib 22 import json 23 import logging 24 import os 25 import sys 26 import traceback 27 import yaml 28 29 import pkg_resources 30 from colorlog import ColoredFormatter 31 32 from sawtooth_cli.exceptions import CliException 33 from sawtooth_cli.rest_client import RestClient 34 35 from sawtooth_cli.protobuf.settings_pb2 import SettingsPayload 36 from sawtooth_cli.protobuf.settings_pb2 import SettingProposal 37 from sawtooth_cli.protobuf.settings_pb2 import SettingVote 38 from sawtooth_cli.protobuf.settings_pb2 import SettingCandidates 39 from sawtooth_cli.protobuf.setting_pb2 import Setting 40 from sawtooth_cli.protobuf.transaction_pb2 import TransactionHeader 41 from sawtooth_cli.protobuf.transaction_pb2 import Transaction 42 from sawtooth_cli.protobuf.batch_pb2 import BatchHeader 43 from sawtooth_cli.protobuf.batch_pb2 import Batch 44 from sawtooth_cli.protobuf.batch_pb2 import BatchList 45 46 from sawtooth_signing import create_context 47 from sawtooth_signing import CryptoFactory 48 from sawtooth_signing import ParseError 49 from sawtooth_signing.secp256k1 import Secp256k1PrivateKey 50 51 DISTRIBUTION_NAME = 'sawset' 52 53 54 SETTINGS_NAMESPACE = '000000' 55 56 _MIN_PRINT_WIDTH = 15 57 _MAX_KEY_PARTS = 4 58 _ADDRESS_PART_SIZE = 16 59 60 61 def add_config_parser(subparsers, parent_parser): 62 """Creates the arg parsers needed for the config command and 63 its subcommands. 64 """ 65 parser = subparsers.add_parser( 66 'config', 67 help='Changes genesis block settings and create, view, and ' 68 'vote on settings proposals', 69 description='Provides subcommands to change genesis block settings ' 70 'and to view, create, and vote on existing proposals.' 71 ) 72 73 config_parsers = parser.add_subparsers(title="subcommands", 74 dest="subcommand") 75 config_parsers.required = True 76 77 78 def _do_config_proposal_create(args): 79 """Executes the 'proposal create' subcommand. Given a key file, and a 80 series of key/value pairs, it generates batches of sawtooth_settings 81 transactions in a BatchList instance. The BatchList is either stored to a 82 file or submitted to a validator, depending on the supplied CLI arguments. 83 """ 84 settings = [s.split('=', 1) for s in args.setting] 85 86 signer = _read_signer(args.key) 87 88 txns = [_create_propose_txn(signer, setting) 89 for setting in settings] 90 91 batch = _create_batch(signer, txns) 92 93 batch_list = BatchList(batches=[batch]) 94 95 if args.output is not None: 96 try: 97 with open(args.output, 'wb') as batch_file: 98 batch_file.write(batch_list.SerializeToString()) 99 except IOError as e: 100 raise CliException( 101 'Unable to write to batch file: {}'.format(str(e))) 102 elif args.url is not None: 103 rest_client = RestClient(args.url) 104 rest_client.send_batches(batch_list) 105 else: 106 raise AssertionError('No target for create set.') 107 108 109 def _do_config_proposal_list(args): 110 """Executes the 'proposal list' subcommand. 111 112 Given a url, optional filters on prefix and public key, this command lists 113 the current pending proposals for settings changes. 114 """ 115 116 def _accept(candidate, public_key, prefix): 117 # Check to see if the first public key matches the given public key 118 # (if it is not None). This public key belongs to the user that 119 # created it. 120 has_pub_key = (not public_key 121 or candidate.votes[0].public_key == public_key) 122 has_prefix = candidate.proposal.setting.startswith(prefix) 123 return has_prefix and has_pub_key 124 125 candidates_payload = _get_proposals(RestClient(args.url)) 126 candidates = [ 127 c for c in candidates_payload.candidates 128 if _accept(c, args.public_key, args.filter) 129 ] 130 131 if args.format == 'default': 132 for candidate in candidates: 133 print('{}: {} => {}'.format( 134 candidate.proposal_id, 135 candidate.proposal.setting, 136 candidate.proposal.value)) 137 elif args.format == 'csv': 138 writer = csv.writer(sys.stdout, quoting=csv.QUOTE_ALL) 139 writer.writerow(['PROPOSAL_ID', 'KEY', 'VALUE']) 140 for candidate in candidates: 141 writer.writerow([ 142 candidate.proposal_id, 143 candidate.proposal.setting, 144 candidate.proposal.value]) 145 elif args.format == 'json' or args.format == 'yaml': 146 candidates_snapshot = \ 147 {c.proposal_id: {c.proposal.setting: c.proposal.value} 148 for c in candidates} 149 150 if args.format == 'json': 151 print(json.dumps(candidates_snapshot, indent=2, sort_keys=True)) 152 else: 153 print(yaml.dump(candidates_snapshot, 154 default_flow_style=False)[0:-1]) 155 else: 156 raise AssertionError('Unknown format {}'.format(args.format)) 157 158 159 def _do_config_proposal_vote(args): 160 """Executes the 'proposal vote' subcommand. Given a key file, a proposal 161 id and a vote value, it generates a batch of sawtooth_settings transactions 162 in a BatchList instance. The BatchList is file or submitted to a 163 validator. 164 """ 165 signer = _read_signer(args.key) 166 rest_client = RestClient(args.url) 167 168 proposals = _get_proposals(rest_client) 169 170 proposal = None 171 for candidate in proposals.candidates: 172 if candidate.proposal_id == args.proposal_id: 173 proposal = candidate 174 break 175 176 if proposal is None: 177 raise CliException('No proposal exists with the given id') 178 179 for vote_record in proposal.votes: 180 if vote_record.public_key == signer.get_public_key().as_hex(): 181 raise CliException( 182 'A vote has already been recorded with this signing key') 183 184 txn = _create_vote_txn( 185 signer, 186 args.proposal_id, 187 proposal.proposal.setting, 188 args.vote_value) 189 batch = _create_batch(signer, [txn]) 190 191 batch_list = BatchList(batches=[batch]) 192 193 rest_client.send_batches(batch_list) 194 195 196 def _do_config_genesis(args): 197 signer = _read_signer(args.key) 198 public_key = signer.get_public_key().as_hex() 199 200 authorized_keys = args.authorized_key if args.authorized_key else \ 201 [public_key] 202 if public_key not in authorized_keys: 203 authorized_keys.append(public_key) 204 205 txns = [] 206 207 txns.append(_create_propose_txn( 208 signer, 209 ('sawtooth.settings.vote.authorized_keys', 210 ','.join(authorized_keys)))) 211 212 if args.approval_threshold is not None: 213 if args.approval_threshold < 1: 214 raise CliException('approval threshold must not be less than 1') 215 216 if args.approval_threshold > len(authorized_keys): 217 raise CliException( 218 'approval threshold must not be greater than the number of ' 219 'authorized keys') 220 221 txns.append(_create_propose_txn( 222 signer, 223 ('sawtooth.settings.vote.approval_threshold', 224 str(args.approval_threshold)))) 225 226 batch = _create_batch(signer, txns) 227 batch_list = BatchList(batches=[batch]) 228 229 try: 230 with open(args.output, 'wb') as batch_file: 231 batch_file.write(batch_list.SerializeToString()) 232 print('Generated {}'.format(args.output)) 233 except IOError as e: 234 raise CliException( 235 'Unable to write to batch file: {}'.format(str(e))) 236 237 238 def _get_proposals(rest_client): 239 state_leaf = rest_client.get_leaf( 240 _key_to_address('sawtooth.settings.vote.proposals')) 241 242 config_candidates = SettingCandidates() 243 244 if state_leaf is not None: 245 setting_bytes = b64decode(state_leaf['data']) 246 setting = Setting() 247 setting.ParseFromString(setting_bytes) 248 249 candidates_bytes = None 250 for entry in setting.entries: 251 if entry.key == 'sawtooth.settings.vote.proposals': 252 candidates_bytes = entry.value 253 254 if candidates_bytes is not None: 255 decoded = b64decode(candidates_bytes) 256 config_candidates.ParseFromString(decoded) 257 258 return config_candidates 259 260 261 def _read_signer(key_filename): 262 """Reads the given file as a hex key. 263 264 Args: 265 key_filename: The filename where the key is stored. If None, 266 defaults to the default key for the current user. 267 268 Returns: 269 Signer: the signer 270 271 Raises: 272 CliException: If unable to read the file. 273 """ 274 filename = key_filename 275 if filename is None: 276 filename = os.path.join(os.path.expanduser('~'), 277 '.sawtooth', 278 'keys', 279 getpass.getuser() + '.priv') 280 281 try: 282 with open(filename, 'r') as key_file: 283 signing_key = key_file.read().strip() 284 except IOError as e: 285 raise CliException('Unable to read key file: {}'.format(str(e))) 286 287 try: 288 private_key = Secp256k1PrivateKey.from_hex(signing_key) 289 except ParseError as e: 290 raise CliException('Unable to read key in file: {}'.format(str(e))) 291 292 context = create_context('secp256k1') 293 crypto_factory = CryptoFactory(context) 294 return crypto_factory.new_signer(private_key) 295 296 297 def _create_batch(signer, transactions): 298 """Creates a batch from a list of transactions and a public key, and signs 299 the resulting batch with the given signing key. 300 301 Args: 302 signer (:obj:`Signer`): The cryptographic signer 303 transactions (list of `Transaction`): The transactions to add to the 304 batch. 305 306 Returns: 307 `Batch`: The constructed and signed batch. 308 """ 309 txn_ids = [txn.header_signature for txn in transactions] 310 batch_header = BatchHeader( 311 signer_public_key=signer.get_public_key().as_hex(), 312 transaction_ids=txn_ids).SerializeToString() 313 314 return Batch( 315 header=batch_header, 316 header_signature=signer.sign(batch_header), 317 transactions=transactions) 318 319 320 def _create_propose_txn(signer, setting_key_value): 321 """Creates an individual sawtooth_settings transaction for the given 322 key and value. 323 """ 324 setting_key, setting_value = setting_key_value 325 nonce = str(datetime.datetime.utcnow().timestamp()) 326 proposal = SettingProposal( 327 setting=setting_key, 328 value=setting_value, 329 nonce=nonce) 330 payload = SettingsPayload(data=proposal.SerializeToString(), 331 action=SettingsPayload.PROPOSE) 332 333 return _make_txn(signer, setting_key, payload) 334 335 336 def _create_vote_txn(signer, proposal_id, setting_key, vote_value): 337 """Creates an individual sawtooth_settings transaction for voting on a 338 proposal for a particular setting key. 339 """ 340 if vote_value == 'accept': 341 vote_id = SettingVote.ACCEPT 342 else: 343 vote_id = SettingVote.REJECT 344 345 vote = SettingVote(proposal_id=proposal_id, vote=vote_id) 346 payload = SettingsPayload(data=vote.SerializeToString(), 347 action=SettingsPayload.VOTE) 348 349 return _make_txn(signer, setting_key, payload) 350 351 352 def _make_txn(signer, setting_key, payload): 353 """Creates and signs a sawtooth_settings transaction with with a payload. 354 """ 355 serialized_payload = payload.SerializeToString() 356 header = TransactionHeader( 357 signer_public_key=signer.get_public_key().as_hex(), 358 family_name='sawtooth_settings', 359 family_version='1.0', 360 inputs=_config_inputs(setting_key), 361 outputs=_config_outputs(setting_key), 362 dependencies=[], 363 payload_sha512=hashlib.sha512(serialized_payload).hexdigest(), 364 batcher_public_key=signer.get_public_key().as_hex() 365 ).SerializeToString() 366 367 return Transaction( 368 header=header, 369 header_signature=signer.sign(header), 370 payload=serialized_payload) 371 372 373 def _config_inputs(key): 374 """Creates the list of inputs for a sawtooth_settings transaction, for a 375 given setting key. 376 """ 377 return [ 378 _key_to_address('sawtooth.settings.vote.proposals'), 379 _key_to_address('sawtooth.settings.vote.authorized_keys'), 380 _key_to_address('sawtooth.settings.vote.approval_threshold'), 381 _key_to_address(key) 382 ] 383 384 385 def _config_outputs(key): 386 """Creates the list of outputs for a sawtooth_settings transaction, for a 387 given setting key. 388 """ 389 return [ 390 _key_to_address('sawtooth.settings.vote.proposals'), 391 _key_to_address(key) 392 ] 393 394 395 def _short_hash(in_str): 396 return hashlib.sha256(in_str.encode()).hexdigest()[:_ADDRESS_PART_SIZE] 397 398 399 def _key_to_address(key): 400 """Creates the state address for a given setting key. 401 """ 402 key_parts = key.split('.', maxsplit=_MAX_KEY_PARTS - 1) 403 key_parts.extend([''] * (_MAX_KEY_PARTS - len(key_parts))) 404 405 return SETTINGS_NAMESPACE + ''.join(_short_hash(x) for x in key_parts) 406 407 408 def setting_key_to_address(key): 409 return _key_to_address(key) 410 411 412 def create_console_handler(verbose_level): 413 clog = logging.StreamHandler() 414 formatter = ColoredFormatter( 415 "%(log_color)s[%(asctime)s %(levelname)-8s%(module)s]%(reset)s " 416 "%(white)s%(message)s", 417 datefmt="%H:%M:%S", 418 reset=True, 419 log_colors={ 420 'DEBUG': 'cyan', 421 'INFO': 'green', 422 'WARNING': 'yellow', 423 'ERROR': 'red', 424 'CRITICAL': 'red', 425 }) 426 427 clog.setFormatter(formatter) 428 429 if verbose_level == 0: 430 clog.setLevel(logging.WARN) 431 elif verbose_level == 1: 432 clog.setLevel(logging.INFO) 433 else: 434 clog.setLevel(logging.DEBUG) 435 436 return clog 437 438 439 def setup_loggers(verbose_level): 440 logger = logging.getLogger() 441 logger.setLevel(logging.DEBUG) 442 logger.addHandler(create_console_handler(verbose_level)) 443 444 445 def create_parent_parser(prog_name): 446 parent_parser = argparse.ArgumentParser(prog=prog_name, add_help=False) 447 parent_parser.add_argument( 448 '-v', '--verbose', 449 action='count', 450 help='enable more verbose output') 451 452 try: 453 version = pkg_resources.get_distribution(DISTRIBUTION_NAME).version 454 except pkg_resources.DistributionNotFound: 455 version = 'UNKNOWN' 456 457 parent_parser.add_argument( 458 '-V', '--version', 459 action='version', 460 version=(DISTRIBUTION_NAME + ' (Hyperledger Sawtooth) version {}') 461 .format(version), 462 help='display version information') 463 464 return parent_parser 465 466 467 def create_parser(prog_name): 468 parent_parser = create_parent_parser(prog_name) 469 470 parser = argparse.ArgumentParser( 471 description='Provides subcommands to change genesis block settings ' 472 'and to view, create, and vote on settings proposals.', 473 parents=[parent_parser]) 474 475 subparsers = parser.add_subparsers(title='subcommands', dest='subcommand') 476 subparsers.required = True 477 478 # The following parser is for the `genesis` subcommand. 479 # This command creates a batch that contains all of the initial 480 # transactions for on-chain settings 481 genesis_parser = subparsers.add_parser( 482 'genesis', 483 help='Creates a genesis batch file of settings transactions', 484 description='Creates a Batch of settings proposals that can be ' 485 'consumed by "sawadm genesis" and used ' 486 'during genesis block construction.' 487 ) 488 genesis_parser.add_argument( 489 '-k', '--key', 490 type=str, 491 help='specify signing key for resulting batches ' 492 'and initial authorized key') 493 494 genesis_parser.add_argument( 495 '-o', '--output', 496 type=str, 497 default='config-genesis.batch', 498 help='specify the output file for the resulting batches') 499 500 genesis_parser.add_argument( 501 '-T', '--approval-threshold', 502 type=int, 503 help='set the number of votes required to enable a setting change') 504 505 genesis_parser.add_argument( 506 '-A', '--authorized-key', 507 type=str, 508 action='append', 509 help='specify a public key for the user authorized to submit ' 510 'config transactions') 511 512 # The following parser is for the `proposal` subcommand group. These 513 # commands allow the user to create proposals which may be applied 514 # immediately or placed in ballot mode, depending on the current on-chain 515 # settings. 516 517 proposal_parser = subparsers.add_parser( 518 'proposal', 519 help='Views, creates, or votes on settings change proposals', 520 description='Provides subcommands to view, create, or vote on ' 521 'proposed settings') 522 proposal_parsers = proposal_parser.add_subparsers( 523 title='subcommands', 524 dest='proposal_cmd') 525 proposal_parsers.required = True 526 527 prop_parser = proposal_parsers.add_parser( 528 'create', 529 help='Creates proposals for setting changes', 530 description='Create proposals for settings changes. The change ' 531 'may be applied immediately or after a series of votes, ' 532 'depending on the vote threshold setting.' 533 ) 534 535 prop_parser.add_argument( 536 '-k', '--key', 537 type=str, 538 help='specify a signing key for the resulting batches') 539 540 prop_target_group = prop_parser.add_mutually_exclusive_group() 541 prop_target_group.add_argument( 542 '-o', '--output', 543 type=str, 544 help='specify the output file for the resulting batches') 545 546 prop_target_group.add_argument( 547 '--url', 548 type=str, 549 help="identify the URL of a validator's REST API", 550 default='http://localhost:8008') 551 552 prop_parser.add_argument( 553 'setting', 554 type=str, 555 nargs='+', 556 help='configuration setting as key/value pair with the ' 557 'format <key>=<value>') 558 559 proposal_list_parser = proposal_parsers.add_parser( 560 'list', 561 help='Lists the currently proposed (not active) settings', 562 description='Lists the currently proposed (not active) settings. ' 563 'Use this list of proposals to find proposals to ' 564 'vote on.') 565 566 proposal_list_parser.add_argument( 567 '--url', 568 type=str, 569 help="identify the URL of a validator's REST API", 570 default='http://localhost:8008') 571 572 proposal_list_parser.add_argument( 573 '--public-key', 574 type=str, 575 default='', 576 help='filter proposals from a particular public key') 577 578 proposal_list_parser.add_argument( 579 '--filter', 580 type=str, 581 default='', 582 help='filter keys that begin with this value') 583 584 proposal_list_parser.add_argument( 585 '--format', 586 default='default', 587 choices=['default', 'csv', 'json', 'yaml'], 588 help='choose the output format') 589 590 vote_parser = proposal_parsers.add_parser( 591 'vote', 592 help='Votes for specific setting change proposals', 593 description='Votes for a specific settings change proposal. Use ' 594 '"sawset proposal list" to find the proposal id.') 595 596 vote_parser.add_argument( 597 '--url', 598 type=str, 599 help="identify the URL of a validator's REST API", 600 default='http://localhost:8008') 601 602 vote_parser.add_argument( 603 '-k', '--key', 604 type=str, 605 help='specify a signing key for the resulting transaction batch') 606 607 vote_parser.add_argument( 608 'proposal_id', 609 type=str, 610 help='identify the proposal to vote on') 611 612 vote_parser.add_argument( 613 'vote_value', 614 type=str, 615 choices=['accept', 'reject'], 616 help='specify the value of the vote') 617 618 return parser 619 620 621 def main(prog_name=os.path.basename(sys.argv[0]), args=None, 622 with_loggers=True): 623 parser = create_parser(prog_name) 624 if args is None: 625 args = sys.argv[1:] 626 args = parser.parse_args(args) 627 628 if with_loggers is True: 629 if args.verbose is None: 630 verbose_level = 0 631 else: 632 verbose_level = args.verbose 633 setup_loggers(verbose_level=verbose_level) 634 635 if args.subcommand == 'proposal' and args.proposal_cmd == 'create': 636 _do_config_proposal_create(args) 637 elif args.subcommand == 'proposal' and args.proposal_cmd == 'list': 638 _do_config_proposal_list(args) 639 elif args.subcommand == 'proposal' and args.proposal_cmd == 'vote': 640 _do_config_proposal_vote(args) 641 elif args.subcommand == 'genesis': 642 _do_config_genesis(args) 643 else: 644 raise CliException( 645 '"{}" is not a valid subcommand of "config"'.format( 646 args.subcommand)) 647 648 649 def main_wrapper(): 650 # pylint: disable=bare-except 651 try: 652 main() 653 except CliException as e: 654 print("Error: {}".format(e), file=sys.stderr) 655 sys.exit(1) 656 except KeyboardInterrupt: 657 pass 658 except BrokenPipeError: 659 sys.stderr.close() 660 except SystemExit as e: 661 raise e 662 except: 663 traceback.print_exc(file=sys.stderr) 664 sys.exit(1)