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)